Merge branch 'master' into fix/video-export

This commit is contained in:
abijeet 2019-01-05 17:42:20 +05:30
commit 8a2c13729e
422 changed files with 17297 additions and 10636 deletions

2
.browserslistrc Normal file
View File

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

View File

@ -48,6 +48,7 @@ GITHUB_APP_ID=false
GITHUB_APP_SECRET=false GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
OKTA_BASE_URL=false OKTA_BASE_URL=false
OKTA_APP_ID=false OKTA_APP_ID=false
OKTA_APP_SECRET=false OKTA_APP_SECRET=false
@ -56,9 +57,16 @@ TWITCH_APP_SECRET=false
GITLAB_APP_ID=false GITLAB_APP_ID=false
GITLAB_APP_SECRET=false GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# External services such as Gravatar and Draw.IO
# Disable default services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false DISABLE_EXTERNAL_SERVICES=false
# Use custom avatar service, Sets fetch URL
# Possible placeholders: ${hash} ${size} ${email}
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
# AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
# LDAP Settings # LDAP Settings
LDAP_SERVER=false LDAP_SERVER=false
@ -67,6 +75,15 @@ LDAP_DN=false
LDAP_PASS=false LDAP_PASS=false
LDAP_USER_FILTER=false LDAP_USER_FILTER=false
LDAP_VERSION=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
# Set this option to disable LDAPS Certificate Verification
LDAP_TLS_INSECURE=false
# Mail settings # Mail settings
MAIL_DRIVER=smtp 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:
- Hosting Method (Nginx/Apache/Docker):
**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

@ -1,6 +1,9 @@
<?php <?php
namespace BookStack; namespace BookStack\Actions;
use BookStack\Auth\User;
use BookStack\Model;
/** /**
* @property string key * @property string key

View File

@ -1,7 +1,7 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Actions;
use BookStack\Activity; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entity; use BookStack\Entities\Entity;
use Session; use Session;
class ActivityService class ActivityService
@ -12,7 +12,7 @@ class ActivityService
/** /**
* ActivityService constructor. * ActivityService constructor.
* @param Activity $activity * @param \BookStack\Actions\Activity $activity
* @param PermissionService $permissionService * @param PermissionService $permissionService
*/ */
public function __construct(Activity $activity, PermissionService $permissionService) public function __construct(Activity $activity, PermissionService $permissionService)

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Actions;
use BookStack\Ownable;
class Comment extends Ownable class Comment extends Ownable
{ {

View File

@ -1,7 +1,6 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Actions;
use BookStack\Comment; use BookStack\Entities\Entity;
use BookStack\Entity;
/** /**
* Class CommentRepo * Class CommentRepo
@ -11,13 +10,13 @@ class CommentRepo
{ {
/** /**
* @var Comment $comment * @var \BookStack\Actions\Comment $comment
*/ */
protected $comment; protected $comment;
/** /**
* CommentRepo constructor. * CommentRepo constructor.
* @param Comment $comment * @param \BookStack\Actions\Comment $comment
*/ */
public function __construct(Comment $comment) public function __construct(Comment $comment)
{ {
@ -27,7 +26,7 @@ class CommentRepo
/** /**
* Get a comment by ID. * Get a comment by ID.
* @param $id * @param $id
* @return Comment|\Illuminate\Database\Eloquent\Model * @return \BookStack\Actions\Comment|\Illuminate\Database\Eloquent\Model
*/ */
public function getById($id) public function getById($id)
{ {
@ -36,9 +35,9 @@ class CommentRepo
/** /**
* Create a new comment on an entity. * Create a new comment on an entity.
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @param array $data * @param array $data
* @return Comment * @return \BookStack\Actions\Comment
*/ */
public function create(Entity $entity, $data = []) public function create(Entity $entity, $data = [])
{ {
@ -53,7 +52,7 @@ class CommentRepo
/** /**
* Update an existing comment. * Update an existing comment.
* @param Comment $comment * @param \BookStack\Actions\Comment $comment
* @param array $input * @param array $input
* @return mixed * @return mixed
*/ */
@ -66,7 +65,7 @@ class CommentRepo
/** /**
* Delete a comment from the system. * Delete a comment from the system.
* @param Comment $comment * @param \BookStack\Actions\Comment $comment
* @return mixed * @return mixed
*/ */
public function delete($comment) public function delete($comment)
@ -76,7 +75,7 @@ class CommentRepo
/** /**
* Get the next local ID relative to the linked entity. * Get the next local ID relative to the linked entity.
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @return int * @return int
*/ */
protected function getNextLocalId(Entity $entity) protected function getNextLocalId(Entity $entity)

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Actions;
use BookStack\Model;
/** /**
* Class Attribute * Class Attribute

View File

@ -1,8 +1,7 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Actions;
use BookStack\Tag; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entity; use BookStack\Entities\Entity;
use BookStack\Services\PermissionService;
/** /**
* Class TagRepo * Class TagRepo
@ -17,9 +16,9 @@ class TagRepo
/** /**
* TagRepo constructor. * TagRepo constructor.
* @param Tag $attr * @param \BookStack\Actions\Tag $attr
* @param Entity $ent * @param \BookStack\Entities\Entity $ent
* @param PermissionService $ps * @param \BookStack\Auth\Permissions\PermissionService $ps
*/ */
public function __construct(Tag $attr, Entity $ent, PermissionService $ps) public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
{ {
@ -107,7 +106,7 @@ class TagRepo
/** /**
* Save an array of tags to an entity * Save an array of tags to an entity
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @param array $tags * @param array $tags
* @return array|\Illuminate\Database\Eloquent\Collection * @return array|\Illuminate\Database\Eloquent\Collection
*/ */
@ -128,7 +127,7 @@ class TagRepo
/** /**
* Create a new Tag instance from user input. * Create a new Tag instance from user input.
* @param $input * @param $input
* @return Tag * @return \BookStack\Actions\Tag
*/ */
protected function newInstanceFromInput($input) protected function newInstanceFromInput($input)
{ {

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Actions;
use BookStack\Model;
class View extends Model class View extends Model
{ {

View File

@ -1,7 +1,7 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Actions;
use BookStack\Entity; use BookStack\Auth\Permissions\PermissionService;
use BookStack\View; use BookStack\Entities\Entity;
class ViewService class ViewService
{ {
@ -10,8 +10,8 @@ class ViewService
/** /**
* ViewService constructor. * ViewService constructor.
* @param View $view * @param \BookStack\Actions\View $view
* @param PermissionService $permissionService * @param \BookStack\Auth\Permissions\PermissionService $permissionService
*/ */
public function __construct(View $view, PermissionService $permissionService) public function __construct(View $view, PermissionService $permissionService)
{ {
@ -50,12 +50,13 @@ class ViewService
* Get the entities with the most views. * Get the entities with the most views.
* @param int $count * @param int $count
* @param int $page * @param int $page
* @param bool|false|array $filterModel * @param Entity|false|array $filterModel
* @param string $action - used for permission checking * @param string $action - used for permission checking
* @return * @return
*/ */
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view') public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
{ {
// TODO - Standardise input filter
$skipCount = $count * $page; $skipCount = $count * $page;
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action) $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
@ -65,7 +66,7 @@ class ViewService
if ($filterModel && is_array($filterModel)) { if ($filterModel && is_array($filterModel)) {
$query->whereIn('viewable_type', $filterModel); $query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) { } else if ($filterModel) {
$query->where('viewable_type', '=', get_class($filterModel)); $query->where('viewable_type', '=', $filterModel->getMorphClass());
} }
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
@ -89,7 +90,7 @@ class ViewService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) { if ($filterModel) {
$query = $query->where('viewable_type', '=', get_class($filterModel)); $query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
} }
$query = $query->where('user_id', '=', $user->id); $query = $query->where('user_id', '=', $user->id);

View File

@ -1,11 +1,11 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Auth\Access;
use BookStack\Notifications\ConfirmEmail; use BookStack\Auth\User;
use BookStack\Repos\UserRepo; use BookStack\Auth\UserRepo;
use Carbon\Carbon;
use BookStack\Exceptions\ConfirmationEmailException; use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\User; use BookStack\Notifications\ConfirmEmail;
use Carbon\Carbon;
use Illuminate\Database\Connection as Database; use Illuminate\Database\Connection as Database;
class EmailConfirmationService class EmailConfirmationService
@ -16,7 +16,7 @@ class EmailConfirmationService
/** /**
* EmailConfirmationService constructor. * EmailConfirmationService constructor.
* @param Database $db * @param Database $db
* @param UserRepo $users * @param \BookStack\Auth\UserRepo $users
*/ */
public function __construct(Database $db, UserRepo $users) public function __construct(Database $db, UserRepo $users)
{ {
@ -27,7 +27,7 @@ class EmailConfirmationService
/** /**
* Create new confirmation for a user, * Create new confirmation for a user,
* Also removes any existing old ones. * Also removes any existing old ones.
* @param User $user * @param \BookStack\Auth\User $user
* @throws ConfirmationEmailException * @throws ConfirmationEmailException
*/ */
public function sendConfirmation(User $user) public function sendConfirmation(User $user)
@ -88,7 +88,7 @@ class EmailConfirmationService
/** /**
* Delete all email confirmations that belong to a user. * Delete all email confirmations that belong to a user.
* @param User $user * @param \BookStack\Auth\User $user
* @return mixed * @return mixed
*/ */
public function deleteConfirmationsByUser(User $user) public function deleteConfirmationsByUser(User $user)

View File

@ -1,4 +1,4 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Auth\Access;
/** /**
* Class Ldap * Class Ldap
@ -92,4 +92,27 @@ class Ldap
{ {
return ldap_bind($ldapConnection, $bindRdn, $bindPassword); return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
} }
/**
* Explode a LDAP dn string into an array of components.
* @param string $dn
* @param int $withAttrib
* @return array
*/
public function explodeDn(string $dn, int $withAttrib)
{
return ldap_explode_dn($dn, $withAttrib);
}
/**
* Escape a string for use in an LDAP filter.
* @param string $value
* @param string $ignore
* @param int $flags
* @return string
*/
public function escape(string $value, string $ignore = "", int $flags = 0)
{
return ldap_escape($value, $ignore, $flags);
}
} }

View File

@ -0,0 +1,385 @@
<?php namespace BookStack\Auth\Access;
use BookStack\Auth\Access;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
/**
* Class LdapService
* Handles any app-specific LDAP tasks.
* @package BookStack\Services
*/
class LdapService
{
protected $ldap;
protected $ldapConnection;
protected $config;
protected $userRepo;
protected $enabled;
/**
* LdapService constructor.
* @param Ldap $ldap
* @param \BookStack\Auth\UserRepo $userRepo
*/
public function __construct(Access\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];
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
* @param $userName
* @return array|null
* @throws LdapException
*/
public function getUserDetails($userName)
{
$emailAttr = $this->config['email_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
if ($user === null) {
return null;
}
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
public function validateUserCredentials(Authenticatable $user, $username, $password)
{
$ldapUser = $this->getUserDetails($username);
if ($ldapUser === null) {
return false;
}
if ($ldapUser['uid'] !== $user->external_auth_id) {
return false;
}
$ldapConnection = $this->getConnection();
try {
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
} catch (\ErrorException $e) {
$ldapBind = false;
}
return $ldapBind;
}
/**
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
* @param $connection
* @throws LdapException
*/
protected function bindSystemUser($connection)
{
$ldapDn = $this->config['dn'];
$ldapPass = $this->config['pass'];
$isAnonymous = ($ldapDn === false || $ldapPass === false);
if ($isAnonymous) {
$ldapBind = $this->ldap->bind($connection);
} else {
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) {
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
}
}
/**
* Get the connection to the LDAP server.
* Creates a new connection if one does not exist.
* @return resource
* @throws LdapException
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) {
return $this->ldapConnection;
}
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) {
array_unshift($ldapServer, '');
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
/*
* Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
* the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
* per handle.
*/
if($this->config['tls_insecure']) {
$this->ldap->setOption(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
}
// Set any required options
if ($this->config['version']) {
$this->ldap->setVersion($ldapConnection, $this->config['version']);
}
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
* @param array $attrs
* @return string
*/
protected function buildFilter($filterString, array $attrs)
{
$newAttrs = [];
foreach ($attrs as $key => $attrText) {
$newKey = '${' . $key . '}';
$newAttrs[$newKey] = $this->ldap->escape($attrText);
}
return strtr($filterString, $newAttrs);
}
/**
* Get the groups a user is a part of on ldap
* @param string $userName
* @return array
* @throws LdapException
*/
public function getUserGroups($userName)
{
$groupsAttr = $this->config['group_attribute'];
$user = $this->getUserWithAttributes($userName, [$groupsAttr]);
if ($user === null) {
return [];
}
$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']);
$groupFilter = 'CN=' . $this->ldap->escape($groupName);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$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 array $userGroupSearchResponse
* @return array
*/
protected function groupFilter(array $userGroupSearchResponse)
{
$groupsAttr = strtolower($this->config['group_attribute']);
$ldapGroups = [];
$count = 0;
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
$count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
}
for ($i=0; $i<$count; $i++) {
$dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$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\Auth\User $user
* @param string $username
* @throws LdapException
*/
public function syncGroups(User $user, string $username)
{
$userLdapGroups = $this->getUserGroups($username);
// 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 \BookStack\Auth\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

@ -1,13 +1,12 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Auth\Access;
use BookStack\Http\Requests\Request; use BookStack\Auth\SocialAccount;
use GuzzleHttp\Exception\ClientException; use BookStack\Auth\UserRepo;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured; use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo; use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\SocialAccount; use Laravel\Socialite\Contracts\User as SocialUser;
class SocialAuthService class SocialAuthService
{ {
@ -16,11 +15,11 @@ class SocialAuthService
protected $socialite; protected $socialite;
protected $socialAccount; 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. * SocialAuthService constructor.
* @param UserRepo $userRepo * @param \BookStack\Auth\UserRepo $userRepo
* @param Socialite $socialite * @param Socialite $socialite
* @param SocialAccount $socialAccount * @param SocialAccount $socialAccount
*/ */
@ -41,7 +40,7 @@ class SocialAuthService
public function startLogIn($socialDriver) public function startLogIn($socialDriver)
{ {
$driver = $this->validateDriver($socialDriver); $driver = $this->validateDriver($socialDriver);
return $this->socialite->driver($driver)->redirect(); return $this->getSocialDriver($driver)->redirect();
} }
/** /**
@ -53,23 +52,18 @@ class SocialAuthService
public function startRegister($socialDriver) public function startRegister($socialDriver)
{ {
$driver = $this->validateDriver($socialDriver); $driver = $this->validateDriver($socialDriver);
return $this->socialite->driver($driver)->redirect(); return $this->getSocialDriver($driver)->redirect();
} }
/** /**
* Handle the social registration process on callback. * Handle the social registration process on callback.
* @param $socialDriver * @param string $socialDriver
* @return \Laravel\Socialite\Contracts\User * @param SocialUser $socialUser
* @throws SocialDriverNotConfigured * @return SocialUser
* @throws UserRegistrationException * @throws UserRegistrationException
*/ */
public function handleRegistrationCallback($socialDriver) public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser)
{ {
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
// Check social account has not already been used // Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) { if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login'); throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
@ -84,17 +78,26 @@ class SocialAuthService
} }
/** /**
* Handle the login process on a oAuth callback. * Get the social user details via the social driver.
* @param $socialDriver * @param string $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return SocialUser
* @throws SocialDriverNotConfigured * @throws SocialDriverNotConfigured
* @throws SocialSignInException
*/ */
public function handleLoginCallback($socialDriver) public function getSocialUser(string $socialDriver)
{ {
$driver = $this->validateDriver($socialDriver); $driver = $this->validateDriver($socialDriver);
// Get user details from social driver return $this->socialite->driver($driver)->user();
$socialUser = $this->socialite->driver($driver)->user(); }
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInAccountNotUsed
*/
public function handleLoginCallback($socialDriver, SocialUser $socialUser)
{
$socialId = $socialUser->getId(); $socialId = $socialUser->getId();
// Get any attached social accounts or users // Get any attached social accounts or users
@ -136,7 +139,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]); $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
} }
throw new SocialSignInException($message, '/login'); throw new SocialSignInAccountNotUsed($message, '/login');
} }
/** /**
@ -199,8 +202,28 @@ class SocialAuthService
} }
/** /**
* @param string $socialDriver * Check if the current config for the given driver allows auto-registration.
* @param \Laravel\Socialite\Contracts\User $socialUser * @param string $driver
* @return bool
*/
public function driverAutoRegisterEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_register') === true;
}
/**
* Check if the current config for the given driver allow email address auto-confirmation.
* @param string $driver
* @return bool
*/
public function driverAutoConfirmEmailEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
}
/**
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialAccount * @return SocialAccount
*/ */
public function fillSocialAccount($socialDriver, $socialUser) public function fillSocialAccount($socialDriver, $socialUser)
@ -224,4 +247,20 @@ class SocialAuthService
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)])); session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl()); return redirect(user()->getEditUrl());
} }
/**
* Provide redirect options per service for the Laravel Socialite driver
* @param $driverName
* @return \Laravel\Socialite\Contracts\Provider
*/
public function getSocialDriver(string $driverName)
{
$driver = $this->socialite->driver($driverName);
if ($driverName === 'google' && config('services.google.select_account')) {
$driver->with(['prompt' => 'select_account']);
}
return $driver;
}
} }

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Auth\Permissions;
use BookStack\Model;
class EntityPermission extends Model class EntityPermission extends Model
{ {

View File

@ -1,4 +1,8 @@
<?php namespace BookStack; <?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Entity;
use BookStack\Model;
class JointPermission extends Model class JointPermission extends Model
{ {

View File

@ -1,14 +1,14 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Auth\Permissions;
use BookStack\Book; use BookStack\Auth\Permissions;
use BookStack\Chapter; use BookStack\Auth\Role;
use BookStack\Entity; use BookStack\Entities\Book;
use BookStack\EntityPermission; use BookStack\Entities\Bookshelf;
use BookStack\JointPermission; use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Page;
use BookStack\Ownable; use BookStack\Ownable;
use BookStack\Page;
use BookStack\Role;
use BookStack\User;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
@ -22,38 +22,53 @@ class PermissionService
protected $userRoles = false; protected $userRoles = false;
protected $currentUserModel = false; protected $currentUserModel = false;
public $book; /**
public $chapter; * @var Connection
public $page; */
protected $db; protected $db;
/**
* @var JointPermission
*/
protected $jointPermission; protected $jointPermission;
/**
* @var Role
*/
protected $role; protected $role;
/**
* @var EntityPermission
*/
protected $entityPermission; protected $entityPermission;
/**
* @var EntityProvider
*/
protected $entityProvider;
protected $entityCache; protected $entityCache;
/** /**
* PermissionService constructor. * PermissionService constructor.
* @param JointPermission $jointPermission * @param JointPermission $jointPermission
* @param EntityPermission $entityPermission * @param EntityPermission $entityPermission
* @param Connection $db
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role * @param Role $role
* @param Connection $db
* @param EntityProvider $entityProvider
*/ */
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role) public function __construct(
{ JointPermission $jointPermission,
Permissions\EntityPermission $entityPermission,
Role $role,
Connection $db,
EntityProvider $entityProvider
) {
$this->db = $db; $this->db = $db;
$this->jointPermission = $jointPermission; $this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission; $this->entityPermission = $entityPermission;
$this->role = $role; $this->role = $role;
$this->book = $book; $this->entityProvider = $entityProvider;
$this->chapter = $chapter;
$this->page = $page;
// TODO - Update so admin still goes through filters
} }
/** /**
@ -67,7 +82,7 @@ class PermissionService
/** /**
* Prepare the local entity cache and ensure it's empty * Prepare the local entity cache and ensure it's empty
* @param Entity[] $entities * @param \BookStack\Entities\Entity[] $entities
*/ */
protected function readyEntityCache($entities = []) protected function readyEntityCache($entities = [])
{ {
@ -93,7 +108,7 @@ class PermissionService
return $this->entityCache['book']->get($bookId); return $this->entityCache['book']->get($bookId);
} }
$book = $this->book->find($bookId); $book = $this->entityProvider->book->find($bookId);
if ($book === null) { if ($book === null) {
$book = false; $book = false;
} }
@ -104,7 +119,7 @@ class PermissionService
/** /**
* Get a chapter via ID, Checks local cache * Get a chapter via ID, Checks local cache
* @param $chapterId * @param $chapterId
* @return Book * @return \BookStack\Entities\Book
*/ */
protected function getChapter($chapterId) protected function getChapter($chapterId)
{ {
@ -112,7 +127,7 @@ class PermissionService
return $this->entityCache['chapter']->get($chapterId); return $this->entityCache['chapter']->get($chapterId);
} }
$chapter = $this->chapter->find($chapterId); $chapter = $this->entityProvider->chapter->find($chapterId);
if ($chapter === null) { if ($chapter === null) {
$chapter = false; $chapter = false;
} }
@ -159,6 +174,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles); $this->buildJointPermissionsForBooks($books, $roles);
}); });
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
} }
/** /**
@ -167,13 +188,28 @@ class PermissionService
*/ */
protected function bookFetchQuery() protected function bookFetchQuery()
{ {
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']); $query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) { }, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']); $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]); }]);
} }
/**
* @param Collection $shelves
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
{
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($shelves->all());
}
$this->createManyJointPermissions($shelves, $roles);
}
/** /**
* Build joint permissions for an array of books * Build joint permissions for an array of books
* @param Collection $books * @param Collection $books
@ -203,7 +239,8 @@ class PermissionService
/** /**
* Rebuild the entity jointPermissions for a particular entity. * Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @throws \Throwable
*/ */
public function buildJointPermissionsForEntity(Entity $entity) public function buildJointPermissionsForEntity(Entity $entity)
{ {
@ -214,7 +251,9 @@ class PermissionService
return; return;
} }
$entities[] = $entity->book; if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity->isA('page') && $entity->chapter_id) { if ($entity->isA('page') && $entity->chapter_id) {
$entities[] = $entity->chapter; $entities[] = $entity->chapter;
@ -226,13 +265,13 @@ class PermissionService
} }
} }
$this->deleteManyJointPermissionsForEntities($entities);
$this->buildJointPermissionsForEntities(collect($entities)); $this->buildJointPermissionsForEntities(collect($entities));
} }
/** /**
* Rebuild the entity jointPermissions for a collection of entities. * Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities * @param Collection $entities
* @throws \Throwable
*/ */
public function buildJointPermissionsForEntities(Collection $entities) public function buildJointPermissionsForEntities(Collection $entities)
{ {
@ -254,6 +293,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles); $this->buildJointPermissionsForBooks($books, $roles);
}); });
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
} }
/** /**
@ -289,7 +334,7 @@ class PermissionService
/** /**
* Delete all of the entity jointPermissions for a list of entities. * Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities * @param \BookStack\Entities\Entity[] $entities
* @throws \Throwable * @throws \Throwable
*/ */
protected function deleteManyJointPermissionsForEntities($entities) protected function deleteManyJointPermissionsForEntities($entities)
@ -370,7 +415,7 @@ class PermissionService
/** /**
* Get the actions related to an entity. * Get the actions related to an entity.
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @return array * @return array
*/ */
protected function getActions(Entity $entity) protected function getActions(Entity $entity)
@ -412,7 +457,7 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
} }
if ($entity->isA('book')) { if ($entity->isA('book') || $entity->isA('bookshelf')) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
} }
@ -456,7 +501,7 @@ class PermissionService
/** /**
* Create an array of data with the information of an entity jointPermissions. * Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion. * Used to build data for bulk insertion.
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @param Role $role * @param Role $role
* @param $action * @param $action
* @param $permissionAll * @param $permissionAll
@ -484,11 +529,6 @@ class PermissionService
*/ */
public function checkOwnableUserAccess(Ownable $ownable, $permission) public function checkOwnableUserAccess(Ownable $ownable, $permission)
{ {
if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission); $explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id); $baseQuery = $ownable->where('id', '=', $ownable->id);
@ -519,7 +559,7 @@ class PermissionService
/** /**
* Check if an entity has restrictions set on itself or its * Check if an entity has restrictions set on itself or its
* parent tree. * parent tree.
* @param Entity $entity * @param \BookStack\Entities\Entity $entity
* @param $action * @param $action
* @return bool|mixed * @return bool|mixed
*/ */
@ -569,7 +609,9 @@ class PermissionService
*/ */
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
{ {
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) { $entities = $this->entityProvider;
$pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0); $query->where('draft', '=', 0);
if (!$filterDrafts) { if (!$filterDrafts) {
$query->orWhere(function ($query) { $query->orWhere(function ($query) {
@ -577,21 +619,20 @@ class PermissionService
}); });
} }
}); });
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id); $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect); ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
if (!$this->isAdmin()) { // Add joint permission filter
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)') $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type') ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles()) ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function ($query) { ->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) { $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id); $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
}); });
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery); });
} $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc'); $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean(); $this->clean();
@ -601,7 +642,7 @@ class PermissionService
/** /**
* Add restrictions for a generic entity * Add restrictions for a generic entity
* @param string $entityType * @param string $entityType
* @param Builder|Entity $query * @param Builder|\BookStack\Entities\Entity $query
* @param string $action * @param string $action
* @return Builder * @return Builder
*/ */
@ -619,11 +660,6 @@ class PermissionService
}); });
} }
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action; $this->currentAction = $action;
return $this->entityRestrictionQuery($query); return $this->entityRestrictionQuery($query);
} }
@ -639,10 +675,6 @@ class PermissionService
*/ */
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view') public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{ {
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action; $this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
@ -675,20 +707,16 @@ class PermissionService
*/ */
public function filterRelatedPages($query, $tableName, $entityIdColumn) public function filterRelatedPages($query, $tableName, $entityIdColumn)
{ {
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view'; $this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$q = $query->where(function ($query) use ($tableDetails) { $pageMorphClass = $this->entityProvider->page->getMorphClass();
$query->where(function ($query) use (&$tableDetails) { $q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) { $query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
$permissionQuery->select('id')->from('joint_permissions') $permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where('entity_type', '=', 'Bookstack\\Page') ->where('entity_type', '=', $pageMorphClass)
->where('action', '=', $this->currentAction) ->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles()) ->whereIn('role_id', $this->getRoles())
->where(function ($query) { ->where(function ($query) {
@ -704,22 +732,9 @@ class PermissionService
return $q; return $q;
} }
/**
* Check if the current user is an admin.
* @return bool
*/
private function isAdmin()
{
if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
}
return $this->isAdminUser;
}
/** /**
* Get the current user * Get the current user
* @return User * @return \BookStack\Auth\User
*/ */
private function currentUser() private function currentUser()
{ {

View File

@ -1,10 +1,8 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\RolePermission;
use BookStack\Role;
use BookStack\Services\PermissionService;
use Setting;
class PermissionsRepo class PermissionsRepo
{ {
@ -19,9 +17,9 @@ class PermissionsRepo
* PermissionsRepo constructor. * PermissionsRepo constructor.
* @param RolePermission $permission * @param RolePermission $permission
* @param Role $role * @param Role $role
* @param PermissionService $permissionService * @param \BookStack\Auth\Permissions\PermissionService $permissionService
*/ */
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService) public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
{ {
$this->permission = $permission; $this->permission = $permission;
$this->role = $role; $this->role = $role;
@ -80,7 +78,7 @@ class PermissionsRepo
/** /**
* Updates an existing role. * Updates an existing role.
* Ensure Admin role always has all permissions. * Ensure Admin role always have core permissions.
* @param $roleId * @param $roleId
* @param $roleData * @param $roleData
* @throws PermissionsException * @throws PermissionsException
@ -90,13 +88,18 @@ class PermissionsRepo
$role = $this->role->findOrFail($roleId); $role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->system_name === 'admin') { if ($role->system_name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray(); $permissions = array_merge($permissions, [
$role->permissions()->sync($permissions); 'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
} }
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData); $role->fill($roleData);
$role->save(); $role->save();
$this->permissionService->buildJointPermissionForRole($role); $this->permissionService->buildJointPermissionForRole($role);

View File

@ -1,4 +1,7 @@
<?php namespace BookStack; <?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Model;
class RolePermission extends Model class RolePermission extends Model
{ {

View File

@ -1,9 +1,12 @@
<?php namespace BookStack; <?php namespace BookStack\Auth;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Model;
class Role extends Model class Role extends Model
{ {
protected $fillable = ['display_name', 'description']; protected $fillable = ['display_name', 'description', 'external_auth_id'];
/** /**
* The roles that belong to the role. * The roles that belong to the role.
@ -27,7 +30,7 @@ class Role extends Model
*/ */
public function permissions() public function permissions()
{ {
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id'); return $this->belongsToMany(Permissions\RolePermission::class, 'permission_role', 'role_id', 'permission_id');
} }
/** /**
@ -48,18 +51,18 @@ class Role extends Model
/** /**
* Add a permission to this role. * Add a permission to this role.
* @param RolePermission $permission * @param \BookStack\Auth\Permissions\RolePermission $permission
*/ */
public function attachPermission(RolePermission $permission) public function attachPermission(Permissions\RolePermission $permission)
{ {
$this->permissions()->attach($permission->id); $this->permissions()->attach($permission->id);
} }
/** /**
* Detach a single permission from this role. * Detach a single permission from this role.
* @param RolePermission $permission * @param \BookStack\Auth\Permissions\RolePermission $permission
*/ */
public function detachPermission(RolePermission $permission) public function detachPermission(Permissions\RolePermission $permission)
{ {
$this->permissions()->detach($permission->id); $this->permissions()->detach($permission->id);
} }

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Auth;
use BookStack\Model;
class SocialAccount extends Model class SocialAccount extends Model
{ {

View File

@ -1,6 +1,8 @@
<?php namespace BookStack; <?php namespace BookStack\Auth;
use BookStack\Model;
use BookStack\Notifications\ResetPassword; use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;

View File

@ -1,10 +1,10 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Auth;
use Activity; use Activity;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Image; use BookStack\Exceptions\UserUpdateException;
use BookStack\Role; use BookStack\Uploads\Image;
use BookStack\User;
use Exception; use Exception;
use Images; use Images;
@ -43,7 +43,7 @@ class UserRepo
*/ */
public function getById($id) public function getById($id)
{ {
return $this->user->findOrFail($id); return $this->user->newQuery()->findOrFail($id);
} }
/** /**
@ -76,33 +76,31 @@ class UserRepo
return $query->paginate($count); return $query->paginate($count);
} }
/** /**
* Creates a new user and attaches a role to them. * Creates a new user and attaches a role to them.
* @param array $data * @param array $data
* @return User * @param boolean $verifyEmail
* @return \BookStack\Auth\User
*/ */
public function registerNew(array $data) public function registerNew(array $data, $verifyEmail = false)
{ {
$user = $this->create($data); $user = $this->create($data, $verifyEmail);
$this->attachDefaultRole($user); $this->attachDefaultRole($user);
$this->downloadAndAssignUserAvatar($user);
// Get avatar from gravatar and save
$this->downloadGravatarToUserAvatar($user);
return $user; return $user;
} }
/** /**
* Give a user the default role. Used when creating a new user. * Give a user the default role. Used when creating a new user.
* @param $user * @param User $user
*/ */
public function attachDefaultRole($user) public function attachDefaultRole(User $user)
{ {
$roleId = setting('registration-role'); $roleId = setting('registration-role');
if ($roleId === false) { if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
$roleId = $this->role->first()->id; $user->attachRoleId($roleId);
} }
$user->attachRoleId($roleId);
} }
/** /**
@ -122,7 +120,7 @@ class UserRepo
/** /**
* Checks if the give user is the only admin. * Checks if the give user is the only admin.
* @param User $user * @param \BookStack\Auth\User $user
* @return bool * @return bool
*/ */
public function isOnlyAdmin(User $user) public function isOnlyAdmin(User $user)
@ -138,24 +136,59 @@ class UserRepo
return true; return true;
} }
/**
* Set the assigned user roles via an array of role IDs.
* @param User $user
* @param array $roles
* @throws UserUpdateException
*/
public function setUserRoles(User $user, array $roles)
{
if ($this->demotingLastAdmin($user, $roles)) {
throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
}
$user->roles()->sync($roles);
}
/**
* Check if the given user is the last admin and their new roles no longer
* contains the admin role.
* @param User $user
* @param array $newRoles
* @return bool
*/
protected function demotingLastAdmin(User $user, array $newRoles) : bool
{
if ($this->isOnlyAdmin($user)) {
$adminRole = $this->role->getSystemRole('admin');
if (!in_array(strval($adminRole->id), $newRoles)) {
return true;
}
}
return false;
}
/** /**
* Create a new basic instance of user. * Create a new basic instance of user.
* @param array $data * @param array $data
* @return User * @param boolean $verifyEmail
* @return \BookStack\Auth\User
*/ */
public function create(array $data) public function create(array $data, $verifyEmail = false)
{ {
return $this->user->forceCreate([ return $this->user->forceCreate([
'name' => $data['name'], 'name' => $data['name'],
'email' => $data['email'], 'email' => $data['email'],
'password' => bcrypt($data['password']), 'password' => bcrypt($data['password']),
'email_confirmed' => false 'email_confirmed' => $verifyEmail
]); ]);
} }
/** /**
* Remove the given user from storage, Delete all related content. * Remove the given user from storage, Delete all related content.
* @param User $user * @param \BookStack\Auth\User $user
* @throws Exception * @throws Exception
*/ */
public function destroy(User $user) public function destroy(User $user)
@ -172,7 +205,7 @@ class UserRepo
/** /**
* Get the latest activity for a user. * Get the latest activity for a user.
* @param User $user * @param \BookStack\Auth\User $user
* @param int $count * @param int $count
* @param int $page * @param int $page
* @return array * @return array
@ -184,7 +217,7 @@ class UserRepo
/** /**
* Get the recently created content for this given user. * Get the recently created content for this given user.
* @param User $user * @param \BookStack\Auth\User $user
* @param int $count * @param int $count
* @return mixed * @return mixed
*/ */
@ -205,15 +238,15 @@ class UserRepo
/** /**
* Get asset created counts for the give user. * Get asset created counts for the give user.
* @param User $user * @param \BookStack\Auth\User $user
* @return array * @return array
*/ */
public function getAssetCounts(User $user) public function getAssetCounts(User $user)
{ {
return [ return [
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(), 'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(), 'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(), 'books' => $this->entityRepo->getUserTotalCreated('book', $user),
]; ];
} }
@ -237,25 +270,24 @@ class UserRepo
} }
/** /**
* Get a gravatar image for a user and set it as their avatar. * Get an avatar image for a user and set it as their avatar.
* Does not run if gravatar disabled in config. * Returns early if avatars disabled or not set in config.
* @param User $user * @param User $user
* @return bool * @return bool
*/ */
public function downloadGravatarToUserAvatar(User $user) public function downloadAndAssignUserAvatar(User $user)
{ {
// Get avatar from gravatar and save if (!Images::avatarFetchEnabled()) {
if (!config('services.gravatar')) {
return false; return false;
} }
try { try {
$avatar = Images::saveUserGravatar($user); $avatar = Images::saveUserAvatar($user);
$user->avatar()->associate($avatar); $user->avatar()->associate($avatar);
$user->save(); $user->save();
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
\Log::error('Failed to save user gravatar image'); \Log::error('Failed to save user avatar image');
return false; return false;
} }
} }

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Services\ImageService; use BookStack\Uploads\ImageService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -30,7 +30,7 @@ class CleanupImages extends Command
/** /**
* Create a new command instance. * Create a new command instance.
* @param ImageService $imageService * @param \BookStack\Uploads\ImageService $imageService
*/ */
public function __construct(ImageService $imageService) public function __construct(ImageService $imageService)
{ {
@ -72,7 +72,9 @@ class CleanupImages extends Command
protected function showDeletedImages($paths) protected function showDeletedImages($paths)
{ {
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return; if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
return;
}
if (count($paths) > 0) { if (count($paths) > 0) {
$this->line('Images to delete:'); $this->line('Images to delete:');
} }

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Activity; use BookStack\Actions\Activity;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class ClearActivity extends Command class ClearActivity extends Command

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\PageRevision; use BookStack\Entities\PageRevision;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class ClearRevisions extends Command class ClearRevisions extends Command

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Repos\UserRepo; use BookStack\Auth\UserRepo;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class CreateAdmin extends Command class CreateAdmin extends Command
@ -76,7 +76,7 @@ class CreateAdmin extends Command
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]); $user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
$this->userRepo->attachSystemRole($user, 'admin'); $this->userRepo->attachSystemRole($user, 'admin');
$this->userRepo->downloadGravatarToUserAvatar($user); $this->userRepo->downloadAndAssignUserAvatar($user);
$user->email_confirmed = true; $user->email_confirmed = true;
$user->save(); $user->save();

View File

@ -2,8 +2,8 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\User; use BookStack\Auth\User;
use BookStack\Repos\UserRepo; use BookStack\Auth\UserRepo;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class DeleteUsers extends Command class DeleteUsers extends Command

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Services\PermissionService; use BookStack\Auth\Permissions\PermissionService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RegeneratePermissions extends Command class RegeneratePermissions extends Command
@ -31,7 +31,7 @@ class RegeneratePermissions extends Command
/** /**
* Create a new command instance. * Create a new command instance.
* *
* @param PermissionService $permissionService * @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
*/ */
public function __construct(PermissionService $permissionService) public function __construct(PermissionService $permissionService)
{ {

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Services\SearchService; use BookStack\Entities\SearchService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RegenerateSearch extends Command class RegenerateSearch extends Command
@ -26,7 +26,7 @@ class RegenerateSearch extends Command
/** /**
* Create a new command instance. * Create a new command instance.
* *
* @param SearchService $searchService * @param \BookStack\Entities\SearchService $searchService
*/ */
public function __construct(SearchService $searchService) public function __construct(SearchService $searchService)
{ {

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Entities;
use BookStack\Uploads\Image;
class Book extends Entity class Book extends Entity
{ {
@ -6,6 +8,15 @@ class Book extends Entity
protected $fillable = ['name', 'description', 'image_id']; protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the morph class for this model.
* @return string
*/
public function getMorphClass()
{
return 'BookStack\\Book';
}
/** /**
* Get the url for this book. * Get the url for this book.
* @param string|bool $path * @param string|bool $path
@ -48,14 +59,6 @@ class Book extends Entity
{ {
return $this->belongsTo(Image::class, 'image_id'); return $this->belongsTo(Image::class, 'image_id');
} }
/*
* Get the edit url for this book.
* @return string
*/
public function getEditUrl()
{
return $this->getUrl() . '/edit';
}
/** /**
* Get all pages within this book. * Get all pages within this book.
@ -75,6 +78,15 @@ class Book extends Entity
return $this->hasMany(Chapter::class); return $this->hasMany(Chapter::class);
} }
/**
* Get the shelves this book is contained within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function shelves()
{
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
}
/** /**
* Get an excerpt of this book's description to the specified length or less. * Get an excerpt of this book's description to the specified length or less.
* @param int $length * @param int $length

View File

@ -0,0 +1,94 @@
<?php namespace BookStack\Entities;
use BookStack\Uploads\Image;
class Bookshelf extends Entity
{
protected $table = 'bookshelves';
public $searchFactor = 3;
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the morph class for this model.
* @return string
*/
public function getMorphClass()
{
return 'BookStack\\Bookshelf';
}
/**
* Get the books in this shelf.
* Should not be used directly since does not take into account permissions.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function books()
{
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
}
/**
* Get the url for this bookshelf.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
if ($path !== false) {
return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return baseUrl('/shelves/' . urlencode($this->slug));
}
/**
* Returns BookShelf cover image, if cover does not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@ -1,4 +1,4 @@
<?php namespace BookStack; <?php namespace BookStack\Entities;
class Chapter extends Entity class Chapter extends Entity
{ {
@ -6,6 +6,15 @@ class Chapter extends Entity
protected $fillable = ['name', 'description', 'priority', 'book_id']; protected $fillable = ['name', 'description', 'priority', 'book_id'];
/**
* Get the morph class for this model.
* @return string
*/
public function getMorphClass()
{
return 'BookStack\\Chapter';
}
/** /**
* Get the book this chapter is within. * Get the book this chapter is within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return \Illuminate\Database\Eloquent\Relations\BelongsTo

View File

@ -1,7 +1,31 @@
<?php namespace BookStack; <?php namespace BookStack\Entities;
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
use BookStack\Actions\Tag;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Ownable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
/**
* Class Entity
* The base class for book-like items such as pages, chapters & books.
* This is not a database model in itself but extended.
*
* @property integer $id
* @property string $name
* @property string $slug
* @property Carbon $created_at
* @property Carbon $updated_at
* @property int $created_by
* @property int $updated_by
* @property boolean $restricted
*
* @package BookStack\Entities
*/
class Entity extends Ownable class Entity extends Ownable
{ {
@ -15,6 +39,17 @@ class Entity extends Ownable
*/ */
public $searchFactor = 1.0; public $searchFactor = 1.0;
/**
* Get the morph class for this model.
* Set here since, due to folder changes, the namespace used
* in the database no longer matches the class namespace.
* @return string
*/
public function getMorphClass()
{
return 'BookStack\\Entity';
}
/** /**
* Compares this entity to another given entity. * Compares this entity to another given entity.
* Matches by comparing class and id. * Matches by comparing class and id.
@ -152,13 +187,13 @@ class Entity extends Ownable
*/ */
public static function getEntityInstance($type) public static function getEntityInstance($type)
{ {
$types = ['Page', 'Book', 'Chapter']; $types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type)); $className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) { if (!in_array($className, $types)) {
return null; return null;
} }
return app('BookStack\\' . $className); return app('BookStack\\Entities\\' . $className);
} }
/** /**
@ -168,10 +203,10 @@ class Entity extends Ownable
*/ */
public function getShortName($length = 25) public function getShortName($length = 25)
{ {
if (strlen($this->name) <= $length) { if (mb_strlen($this->name) <= $length) {
return $this->name; return $this->name;
} }
return substr($this->name, 0, $length - 3) . '...'; return mb_substr($this->name, 0, $length - 3) . '...';
} }
/** /**

View File

@ -0,0 +1,89 @@
<?php namespace BookStack\Entities;
/**
* Class EntityProvider
*
* Provides access to the core entity models.
* Wrapped up in this provider since they are often used together
* so this is a neater alternative to injecting all in individually.
*
* @package BookStack\Entities
*/
class EntityProvider
{
/**
* @var Bookshelf
*/
public $bookshelf;
/**
* @var Book
*/
public $book;
/**
* @var Chapter
*/
public $chapter;
/**
* @var Page
*/
public $page;
/**
* @var PageRevision
*/
public $pageRevision;
/**
* EntityProvider constructor.
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param PageRevision $pageRevision
*/
public function __construct(
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page,
PageRevision $pageRevision
) {
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->pageRevision = $pageRevision;
}
/**
* Fetch all core entity types as an associated array
* with their basic names as the keys.
* @return Entity[]
*/
public function all()
{
return [
'bookshelf' => $this->bookshelf,
'book' => $this->book,
'chapter' => $this->chapter,
'page' => $this->page,
];
}
/**
* Get an entity instance by it's basic name.
* @param string $type
* @return Entity
*/
public function get(string $type)
{
$type = strtolower($type);
return $this->all()[$type];
}
}

View File

@ -1,9 +1,7 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Entities;
use BookStack\Book; use BookStack\Entities\Repos\EntityRepo;
use BookStack\Chapter; use BookStack\Uploads\ImageService;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class ExportService class ExportService
{ {
@ -19,7 +17,8 @@ class ExportService
/** /**
* ExportService constructor. * ExportService constructor.
* @param $entityRepo * @param EntityRepo $entityRepo
* @param ImageService $imageService
*/ */
public function __construct(EntityRepo $entityRepo, ImageService $imageService) public function __construct(EntityRepo $entityRepo, ImageService $imageService)
{ {
@ -30,7 +29,7 @@ class ExportService
/** /**
* Convert a page to a self-contained HTML file. * Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML. * Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page * @param \BookStack\Entities\Page $page
* @return mixed|string * @return mixed|string
* @throws \Throwable * @throws \Throwable
*/ */
@ -45,7 +44,7 @@ class ExportService
/** /**
* Convert a chapter to a self-contained HTML file. * Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter * @param \BookStack\Entities\Chapter $chapter
* @return mixed|string * @return mixed|string
* @throws \Throwable * @throws \Throwable
*/ */
@ -95,7 +94,7 @@ class ExportService
/** /**
* Convert a chapter to a PDF file. * Convert a chapter to a PDF file.
* @param Chapter $chapter * @param \BookStack\Entities\Chapter $chapter
* @return mixed|string * @return mixed|string
* @throws \Throwable * @throws \Throwable
*/ */
@ -114,7 +113,7 @@ class ExportService
/** /**
* Convert a book to a PDF file * Convert a book to a PDF file
* @param Book $book * @param \BookStack\Entities\Book $book
* @return string * @return string
* @throws \Throwable * @throws \Throwable
*/ */
@ -236,7 +235,7 @@ class ExportService
/** /**
* Convert a chapter into a plain text string. * Convert a chapter into a plain text string.
* @param Chapter $chapter * @param \BookStack\Entities\Chapter $chapter
* @return string * @return string
*/ */
public function chapterToPlainText(Chapter $chapter) public function chapterToPlainText(Chapter $chapter)

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Entities;
use BookStack\Uploads\Attachment;
class Page extends Entity class Page extends Entity
{ {
@ -8,6 +10,15 @@ class Page extends Entity
public $textField = 'text'; public $textField = 'text';
/**
* Get the morph class for this model.
* @return string
*/
public function getMorphClass()
{
return 'BookStack\\Page';
}
/** /**
* Converts this page into a simplified array. * Converts this page into a simplified array.
* @return mixed * @return mixed
@ -28,6 +39,15 @@ class Page extends Entity
return $this->belongsTo(Book::class); 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. * Get the chapter that this page is in, If applicable.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
@ -103,4 +123,13 @@ class Page extends Entity
$htmlQuery = $withContent ? 'html' : "'' as html"; $htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at"; return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
} }
/**
* Get the current revision for the page if existing
* @return \BookStack\Entities\PageRevision|null
*/
public function getCurrentRevision()
{
return $this->revisions()->first();
}
} }

View File

@ -1,4 +1,7 @@
<?php namespace BookStack; <?php namespace BookStack\Entities;
use BookStack\Auth\User;
use BookStack\Model;
class PageRevision extends Model class PageRevision extends Model
{ {

View File

@ -0,0 +1,508 @@
<?php namespace BookStack\Entities\Repos;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use Carbon\Carbon;
use DOMDocument;
use DOMXPath;
class PageRepo extends EntityRepo
{
/**
* Get page by slug.
* @param string $pageSlug
* @param string $bookSlug
* @return Page
* @throws \BookStack\Exceptions\NotFoundException
*/
public function getPageBySlug(string $pageSlug, string $bookSlug)
{
return $this->getBySlug('page', $pageSlug, $bookSlug);
}
/**
* Search through page revisions and retrieve the last page in the
* current book that has a slug equal to the one given.
* @param string $pageSlug
* @param string $bookSlug
* @return null|Page
*/
public function getPageByOldSlug(string $pageSlug, string $bookSlug)
{
$revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
->whereHas('page', function ($query) {
$this->permissionService->enforceEntityRestrictions('page', $query);
})
->where('type', '=', 'version')
->where('book_slug', '=', $bookSlug)
->orderBy('created_at', 'desc')
->with('page')->first();
return $revision !== null ? $revision->page : null;
}
/**
* Updates a page with any fillable data and saves it into the database.
* @param Page $page
* @param int $book_id
* @param array $input
* @return Page
* @throws \Exception
*/
public function updatePage(Page $page, int $book_id, array $input)
{
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
// Prevent slug being updated if no name change
if ($page->name !== $input['name']) {
$page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
}
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
}
// Update with new details
$userId = user()->id;
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = $this->pageToPlainText($page);
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->updated_by = $userId;
$page->revision_count++;
$page->save();
// Remove all update drafts for this user & page.
$this->userUpdatePageDraftsQuery($page, $userId)->delete();
// Save a revision after updating
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
$this->savePageRevision($page, $input['summary']);
}
$this->searchService->indexEntity($page);
return $page;
}
/**
* Saves a page revision into the system.
* @param Page $page
* @param null|string $summary
* @return PageRevision
* @throws \Exception
*/
public function savePageRevision(Page $page, string $summary = null)
{
$revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
$revision->created_by = user()->id;
$revision->created_at = $page->updated_at;
$revision->type = 'version';
$revision->summary = $summary;
$revision->revision_number = $page->revision_count;
$revision->save();
$revisionLimit = config('app.revision_limit');
if ($revisionLimit !== false) {
$revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
if ($revisionsToDelete->count() > 0) {
$this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
}
}
return $revision;
}
/**
* Formats a page's html to be tagged correctly
* within the system.
* @param string $htmlText
* @return string
*/
protected function formatHtml(string $htmlText)
{
if ($htmlText == '') {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$idArray = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') {
continue;
}
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
$idArray[] = $id;
continue;
};
}
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (in_array($newId, $idArray)) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
}
// Generate inner html as a string
$html = '';
foreach ($childNodes as $childNode) {
$html .= $doc->saveHTML($childNode);
}
return $html;
}
/**
* Get the plain text version of a page's content.
* @param \BookStack\Entities\Page $page
* @return string
*/
public function pageToPlainText(Page $page)
{
$html = $this->renderPage($page);
return strip_tags($html);
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|null $chapter
* @return \BookStack\Entities\Page
* @throws \Throwable
*/
public function getDraftPage(Book $book, Chapter $chapter = null)
{
$page = $this->entityProvider->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->entityProvider->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Save a page update draft.
* @param Page $page
* @param array $data
* @return PageRevision|Page
*/
public function updatePageDraft(Page $page, array $data = [])
{
// If the page itself is a draft simply update that
if ($page->draft) {
$page->fill($data);
if (isset($data['html'])) {
$page->text = $this->pageToPlainText($page);
}
$page->save();
return $page;
}
// Otherwise save the data to a revision
$userId = user()->id;
$drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
if ($drafts->count() > 0) {
$draft = $drafts->first();
} else {
$draft = $this->entityProvider->pageRevision->newInstance();
$draft->page_id = $page->id;
$draft->slug = $page->slug;
$draft->book_slug = $page->book->slug;
$draft->created_by = $userId;
$draft->type = 'update_draft';
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
$draft->save();
return $draft;
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
* @param Page $draftPage
* @param array $input
* @return Page
* @throws \Exception
*/
public function publishPageDraft(Page $draftPage, array $input)
{
$draftPage->fill($input);
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
}
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = $this->pageToPlainText($draftPage);
$draftPage->draft = false;
$draftPage->revision_count = 1;
$draftPage->save();
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
$this->searchService->indexEntity($draftPage);
return $draftPage;
}
/**
* The base query for getting user update drafts.
* @param Page $page
* @param $userId
* @return mixed
*/
protected function userUpdatePageDraftsQuery(Page $page, int $userId)
{
return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
->where('type', 'update_draft')
->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc');
}
/**
* Get the latest updated draft revision for a particular page and user.
* @param Page $page
* @param $userId
* @return PageRevision|null
*/
public function getUserPageDraft(Page $page, int $userId)
{
return $this->userUpdatePageDraftsQuery($page, $userId)->first();
}
/**
* Get the notification message that informs the user that they are editing a draft page.
* @param PageRevision $draft
* @return string
*/
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
return $message;
}
return $message . "\n" . trans('entities.pages_draft_edited_notification');
}
/**
* A query to check for active update drafts on a particular page.
* @param Page $page
* @param int $minRange
* @return mixed
*/
protected function activePageEditingQuery(Page $page, int $minRange = null)
{
$query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
->where('page_id', '=', $page->id)
->where('updated_at', '>', $page->updated_at)
->where('created_by', '!=', user()->id)
->with('createdBy');
if ($minRange !== null) {
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
}
return $query;
}
/**
* Check if a page is being actively editing.
* Checks for edits since last page updated.
* Passing in a minuted range will check for edits
* within the last x minutes.
* @param Page $page
* @param int $minRange
* @return bool
*/
public function isPageEditingActive(Page $page, int $minRange = null)
{
$draftSearch = $this->activePageEditingQuery($page, $minRange);
return $draftSearch->count() > 0;
}
/**
* Get a notification message concerning the editing activity on a particular page.
* @param Page $page
* @param int $minRange
* @return string
*/
public function getPageEditingActiveMessage(Page $page, int $minRange = null)
{
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
$userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
$timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
}
/**
* Parse the headers on the page to get a navigation menu
* @param string $pageContent
* @return array
*/
public function getPageNav(string $pageContent)
{
if ($pageContent == '') {
return [];
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
if (is_null($headers)) {
return [];
}
$tree = collect([]);
foreach ($headers as $header) {
$text = $header->nodeValue;
$tree->push([
'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)),
'link' => '#' . $header->getAttribute('id'),
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
]);
}
// Normalise headers if only smaller headers have been used
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
}
return $tree->toArray();
}
/**
* Restores a revision's content back into a page.
* @param Page $page
* @param Book $book
* @param int $revisionId
* @return Page
* @throws \Exception
*/
public function restorePageRevision(Page $page, Book $book, int $revisionId)
{
$page->revision_count++;
$this->savePageRevision($page);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
$page->text = $this->pageToPlainText($page);
$page->updated_by = user()->id;
$page->save();
$this->searchService->indexEntity($page);
return $page;
}
/**
* Change the page's parent to the given entity.
* @param Page $page
* @param Entity $parent
* @throws \Throwable
*/
public function changePageParent(Page $page, Entity $parent)
{
$book = $parent->isA('book') ? $parent : $parent->book;
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
$page->save();
if ($page->book->id !== $book->id) {
$page = $this->changeBook('page', $book->id, $page);
}
$page->load('book');
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Create a copy of a page in a new location with a new name.
* @param \BookStack\Entities\Page $page
* @param \BookStack\Entities\Entity $newParent
* @param string $newName
* @return \BookStack\Entities\Page
* @throws \Throwable
*/
public function copyPage(Page $page, Entity $newParent, string $newName = '')
{
$newBook = $newParent->isA('book') ? $newParent : $newParent->book;
$newChapter = $newParent->isA('chapter') ? $newParent : null;
$copyPage = $this->getDraftPage($newBook, $newChapter);
$pageData = $page->getAttributes();
// Update name
if (!empty($newName)) {
$pageData['name'] = $newName;
}
// Copy tags from previous page if set
if ($page->tags) {
$pageData['tags'] = [];
foreach ($page->tags as $tag) {
$pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
}
}
// Set priority
if ($newParent->isA('chapter')) {
$pageData['priority'] = $this->getNewChapterPriority($newParent);
} else {
$pageData['priority'] = $this->getNewBookPriority($newParent);
}
return $this->publishPageDraft($copyPage, $pageData);
}
}

View File

@ -1,24 +1,34 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Entities;
use BookStack\Book; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
use BookStack\SearchTerm;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class SearchService class SearchService
{ {
/**
* @var SearchTerm
*/
protected $searchTerm; protected $searchTerm;
protected $book;
protected $chapter; /**
protected $page; * @var EntityProvider
*/
protected $entityProvider;
/**
* @var Connection
*/
protected $db; protected $db;
/**
* @var PermissionService
*/
protected $permissionService; protected $permissionService;
protected $entities;
/** /**
* Acceptable operators to be used in a query * Acceptable operators to be used in a query
@ -29,24 +39,15 @@ class SearchService
/** /**
* SearchService constructor. * SearchService constructor.
* @param SearchTerm $searchTerm * @param SearchTerm $searchTerm
* @param Book $book * @param EntityProvider $entityProvider
* @param Chapter $chapter
* @param Page $page
* @param Connection $db * @param Connection $db
* @param PermissionService $permissionService * @param PermissionService $permissionService
*/ */
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService) public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
{ {
$this->searchTerm = $searchTerm; $this->searchTerm = $searchTerm;
$this->book = $book; $this->entityProvider = $entityProvider;
$this->chapter = $chapter;
$this->page = $page;
$this->db = $db; $this->db = $db;
$this->entities = [
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
];
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
} }
@ -65,12 +66,13 @@ class SearchService
* @param string $entityType * @param string $entityType
* @param int $page * @param int $page
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed. * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @param string $action
* @return array[int, Collection]; * @return array[int, Collection];
*/ */
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view') public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
{ {
$terms = $this->parseSearchString($searchString); $terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities); $entityTypes = array_keys($this->entityProvider->all());
$entityTypesToSearch = $entityTypes; $entityTypesToSearch = $entityTypes;
if ($entityType !== 'all') { if ($entityType !== 'all') {
@ -167,17 +169,17 @@ class SearchService
* @param array $terms * @param array $terms
* @param string $entityType * @param string $entityType
* @param string $action * @param string $action
* @return \Illuminate\Database\Eloquent\Builder * @return EloquentBuilder
*/ */
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view') protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
{ {
$entity = $this->getEntity($entityType); $entity = $this->entityProvider->get($entityType);
$entitySelect = $entity->newQuery(); $entitySelect = $entity->newQuery();
// Handle normal search terms // Handle normal search terms
if (count($terms['search']) > 0) { if (count($terms['search']) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType)); $subQuery->where('entity_type', '=', $entity->getMorphClass());
$subQuery->where(function (Builder $query) use ($terms) { $subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) { foreach ($terms['search'] as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%'); $query->orWhere('term', 'like', $inputTerm .'%');
@ -191,9 +193,9 @@ class SearchService
// Handle exact term matching // Handle exact term matching
if (count($terms['exact']) > 0) { if (count($terms['exact']) > 0) {
$entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) { $entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) {
foreach ($terms['exact'] as $inputTerm) { foreach ($terms['exact'] as $inputTerm) {
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) { $query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%') $query->where('name', 'like', '%'.$inputTerm .'%')
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
}); });
@ -281,14 +283,14 @@ class SearchService
/** /**
* Apply a tag search term onto a entity query. * Apply a tag search term onto a entity query.
* @param \Illuminate\Database\Eloquent\Builder $query * @param EloquentBuilder $query
* @param string $tagTerm * @param string $tagTerm
* @return mixed * @return mixed
*/ */
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
{ {
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
$query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) { $query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
$tagName = $tagSplit[1]; $tagName = $tagSplit[1];
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : ''; $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
@ -313,16 +315,6 @@ class SearchService
return $query; return $query;
} }
/**
* Get an entity instance via type.
* @param $type
* @return Entity
*/
protected function getEntity($type)
{
return $this->entities[strtolower($type)];
}
/** /**
* Index the given entity. * Index the given entity.
* @param Entity $entity * @param Entity $entity
@ -342,7 +334,7 @@ class SearchService
/** /**
* Index multiple Entities at once * Index multiple Entities at once
* @param Entity[] $entities * @param \BookStack\Entities\Entity[] $entities
*/ */
protected function indexEntities($entities) protected function indexEntities($entities)
{ {
@ -370,20 +362,12 @@ class SearchService
{ {
$this->searchTerm->truncate(); $this->searchTerm->truncate();
// Chunk through all books foreach ($this->entityProvider->all() as $entityModel) {
$this->book->chunk(1000, function ($books) { $selectFields = ['id', 'name', $entityModel->textField];
$this->indexEntities($books); $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
}); $this->indexEntities($entities);
});
// Chunk through all chapters }
$this->chapter->chunk(1000, function ($chapters) {
$this->indexEntities($chapters);
});
// Chunk through all pages
$this->page->chunk(1000, function ($pages) {
$this->indexEntities($pages);
});
} }
/** /**
@ -432,7 +416,7 @@ class SearchService
* Custom entity search filters * Custom entity search filters
*/ */
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input)
{ {
try { try {
$date = date_create($input); $date = date_create($input);
@ -442,7 +426,7 @@ class SearchService
$query->where('updated_at', '>=', $date); $query->where('updated_at', '>=', $date);
} }
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterUpdatedBefore(EloquentBuilder $query, Entity $model, $input)
{ {
try { try {
$date = date_create($input); $date = date_create($input);
@ -452,7 +436,7 @@ class SearchService
$query->where('updated_at', '<', $date); $query->where('updated_at', '<', $date);
} }
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterCreatedAfter(EloquentBuilder $query, Entity $model, $input)
{ {
try { try {
$date = date_create($input); $date = date_create($input);
@ -462,7 +446,7 @@ class SearchService
$query->where('created_at', '>=', $date); $query->where('created_at', '>=', $date);
} }
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterCreatedBefore(EloquentBuilder $query, Entity $model, $input)
{ {
try { try {
$date = date_create($input); $date = date_create($input);
@ -472,7 +456,7 @@ class SearchService
$query->where('created_at', '<', $date); $query->where('created_at', '<', $date);
} }
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
{ {
if (!is_numeric($input) && $input !== 'me') { if (!is_numeric($input) && $input !== 'me') {
return; return;
@ -483,7 +467,7 @@ class SearchService
$query->where('created_by', '=', $input); $query->where('created_by', '=', $input);
} }
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input)
{ {
if (!is_numeric($input) && $input !== 'me') { if (!is_numeric($input) && $input !== 'me') {
return; return;
@ -494,41 +478,41 @@ class SearchService
$query->where('updated_by', '=', $input); $query->where('updated_by', '=', $input);
} }
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterInName(EloquentBuilder $query, Entity $model, $input)
{ {
$query->where('name', 'like', '%' .$input. '%'); $query->where('name', 'like', '%' .$input. '%');
} }
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterInTitle(EloquentBuilder $query, Entity $model, $input)
{ {
$this->filterInName($query, $model, $input); $this->filterInName($query, $model, $input);
} }
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterInBody(EloquentBuilder $query, Entity $model, $input)
{ {
$query->where($model->textField, 'like', '%' .$input. '%'); $query->where($model->textField, 'like', '%' .$input. '%');
} }
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input)
{ {
$query->where('restricted', '=', true); $query->where('restricted', '=', true);
} }
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterViewedByMe(EloquentBuilder $query, Entity $model, $input)
{ {
$query->whereHas('views', function ($query) { $query->whereHas('views', function ($query) {
$query->where('user_id', '=', user()->id); $query->where('user_id', '=', user()->id);
}); });
} }
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterNotViewedByMe(EloquentBuilder $query, Entity $model, $input)
{ {
$query->whereDoesntHave('views', function ($query) { $query->whereDoesntHave('views', function ($query) {
$query->where('user_id', '=', user()->id); $query->where('user_id', '=', user()->id);
}); });
} }
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) protected function filterSortBy(EloquentBuilder $query, Entity $model, $input)
{ {
$functionName = camel_case('sort_by_' . $input); $functionName = camel_case('sort_by_' . $input);
if (method_exists($this, $functionName)) { if (method_exists($this, $functionName)) {
@ -541,7 +525,7 @@ class SearchService
* Sorting filter options * Sorting filter options
*/ */
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model) protected function sortByLastCommented(EloquentBuilder $query, Entity $model)
{ {
$commentsTable = $this->db->getTablePrefix() . 'comments'; $commentsTable = $this->db->getTablePrefix() . 'comments';
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass()); $morphClass = str_replace('\\', '\\\\', $model->getMorphClass());

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Entities;
use BookStack\Model;
class SearchTerm extends Model class SearchTerm extends Model
{ {

View File

@ -3,12 +3,12 @@
namespace BookStack\Exceptions; namespace BookStack\Exceptions;
use Exception; use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler

View File

@ -0,0 +1,5 @@
<?php namespace BookStack\Exceptions;
use Exception;
class HttpFetchException extends Exception {}

View File

@ -11,7 +11,7 @@ class NotifyException extends \Exception
* @param string $message * @param string $message
* @param string $redirectLocation * @param string $redirectLocation
*/ */
public function __construct($message, $redirectLocation) public function __construct(string $message, string $redirectLocation = "/")
{ {
$this->message = $message; $this->message = $message;
$this->redirectLocation = $redirectLocation; $this->redirectLocation = $redirectLocation;

View File

@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialSignInAccountNotUsed extends SocialSignInException
{
}

View File

@ -0,0 +1,3 @@
<?php namespace BookStack\Exceptions;
class UserUpdateException extends NotifyException {}

View File

@ -1,4 +1,4 @@
<?php namespace BookStack\Services\Facades; <?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;

View File

@ -1,4 +1,4 @@
<?php namespace BookStack\Services\Facades; <?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;

View File

@ -1,4 +1,4 @@
<?php namespace BookStack\Services\Facades; <?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;

View File

@ -1,4 +1,4 @@
<?php namespace BookStack\Services\Facades; <?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;

View File

@ -1,10 +1,10 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo; use BookStack\Uploads\Attachment;
use BookStack\Services\AttachmentService; use BookStack\Uploads\AttachmentService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class AttachmentController extends Controller class AttachmentController extends Controller
@ -15,7 +15,7 @@ class AttachmentController extends Controller
/** /**
* AttachmentController constructor. * AttachmentController constructor.
* @param AttachmentService $attachmentService * @param \BookStack\Uploads\AttachmentService $attachmentService
* @param Attachment $attachment * @param Attachment $attachment
* @param EntityRepo $entityRepo * @param EntityRepo $entityRepo
*/ */
@ -201,10 +201,7 @@ class AttachmentController extends Controller
} }
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment); $attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return response($attachmentContents, 200, [ return $this->downloadResponse($attachmentContents, $attachment->getFileName());
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
]);
} }
/** /**

View File

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

View File

@ -2,20 +2,19 @@
namespace BookStack\Http\Controllers\Auth; namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\ConfirmationEmailException; use BookStack\Auth\SocialAccount;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo; use BookStack\Http\Controllers\Controller;
use BookStack\Services\EmailConfirmationService;
use BookStack\Services\SocialAuthService;
use BookStack\SocialAccount;
use BookStack\User;
use Exception; use Exception;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Laravel\Socialite\Contracts\User as SocialUser;
use Validator; use Validator;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
class RegisterController extends Controller class RegisterController extends Controller
{ {
@ -47,11 +46,11 @@ class RegisterController extends Controller
/** /**
* Create a new controller instance. * Create a new controller instance.
* *
* @param SocialAuthService $socialAuthService * @param \BookStack\Auth\Access\SocialAuthService $socialAuthService
* @param EmailConfirmationService $emailConfirmationService * @param \BookStack\Auth\EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo * @param \BookStack\Auth\UserRepo $userRepo
*/ */
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) public function __construct(\BookStack\Auth\Access\SocialAuthService $socialAuthService, \BookStack\Auth\Access\EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{ {
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']); $this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
$this->socialAuthService = $socialAuthService; $this->socialAuthService = $socialAuthService;
@ -118,7 +117,7 @@ class RegisterController extends Controller
/** /**
* Create a new user instance after a valid registration. * Create a new user instance after a valid registration.
* @param array $data * @param array $data
* @return User * @return \BookStack\Auth\User
*/ */
protected function create(array $data) protected function create(array $data)
{ {
@ -133,25 +132,28 @@ class RegisterController extends Controller
* The registrations flow for all users. * The registrations flow for all users.
* @param array $userData * @param array $userData
* @param bool|false|SocialAccount $socialAccount * @param bool|false|SocialAccount $socialAccount
* @param bool $emailVerified
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException * @throws UserRegistrationException
*/ */
protected function registerUser(array $userData, $socialAccount = false) protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
{ {
if (setting('registration-restrict')) { $registrationRestrict = setting('registration-restrict');
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1); $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) { if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register'); throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
} }
} }
$newUser = $this->userRepo->registerNew($userData); $newUser = $this->userRepo->registerNew($userData, $emailVerified);
if ($socialAccount) { if ($socialAccount) {
$newUser->socialAccounts()->save($socialAccount); $newUser->socialAccounts()->save($socialAccount);
} }
if (setting('registration-confirmation') || setting('registration-restrict')) { if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
$newUser->save(); $newUser->save();
try { try {
@ -250,7 +252,6 @@ class RegisterController extends Controller
* @throws SocialSignInException * @throws SocialSignInException
* @throws UserRegistrationException * @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured * @throws \BookStack\Exceptions\SocialDriverNotConfigured
* @throws ConfirmationEmailException
*/ */
public function socialCallback($socialDriver, Request $request) public function socialCallback($socialDriver, Request $request)
{ {
@ -267,12 +268,24 @@ class RegisterController extends Controller
} }
$action = session()->pull('social-callback'); $action = session()->pull('social-callback');
// Attempt login or fall-back to register if allowed.
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
if ($action == 'login') { if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver); try {
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
} catch (SocialSignInAccountNotUsed $exception) {
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
throw $exception;
}
} }
if ($action == 'register') { if ($action == 'register') {
return $this->socialRegisterCallback($socialDriver); return $this->socialRegisterCallback($socialDriver, $socialUser);
} }
return redirect()->back(); return redirect()->back();
} }
@ -288,15 +301,16 @@ class RegisterController extends Controller
/** /**
* Register a new user after a registration callback. * Register a new user after a registration callback.
* @param $socialDriver * @param string $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException * @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/ */
protected function socialRegisterCallback($socialDriver) protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
{ {
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver); $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser); $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance // Create an array of the user data to create a new user instance
$userData = [ $userData = [
@ -304,6 +318,6 @@ class RegisterController extends Controller
'email' => $socialUser->getEmail(), 'email' => $socialUser->getEmail(),
'password' => str_random(30) 'password' => str_random(30)
]; ];
return $this->registerUser($userData, $socialAccount); return $this->registerUser($userData, $socialAccount, $emailVerified);
} }
} }

View File

@ -1,10 +1,10 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Book; use BookStack\Auth\UserRepo;
use BookStack\Repos\EntityRepo; use BookStack\Entities\Book;
use BookStack\Repos\UserRepo; use BookStack\Entities\Repos\EntityRepo;
use BookStack\Services\ExportService; use BookStack\Entities\ExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
@ -19,8 +19,8 @@ class BookController extends Controller
/** /**
* BookController constructor. * BookController constructor.
* @param EntityRepo $entityRepo * @param EntityRepo $entityRepo
* @param UserRepo $userRepo * @param \BookStack\Auth\UserRepo $userRepo
* @param ExportService $exportService * @param \BookStack\Entities\ExportService $exportService
*/ */
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService) public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{ {
@ -204,7 +204,7 @@ class BookController extends Controller
// Get the books involved in the sort // Get the books involved in the sort
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray(); $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
$booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get(); $booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
// Throw permission error if invalid ids or inaccessible books given. // Throw permission error if invalid ids or inaccessible books given.
if (count($bookIdsInvolved) !== count($booksInvolved)) { if (count($bookIdsInvolved) !== count($booksInvolved)) {
$this->showPermissionError(); $this->showPermissionError();
@ -299,10 +299,7 @@ class BookController extends Controller
{ {
$book = $this->entityRepo->getBySlug('book', $bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$pdfContent = $this->exportService->bookToPdf($book); $pdfContent = $this->exportService->bookToPdf($book);
return response()->make($pdfContent, 200, [ return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
]);
} }
/** /**
@ -314,10 +311,7 @@ class BookController extends Controller
{ {
$book = $this->entityRepo->getBySlug('book', $bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToContainedHtml($book); $htmlContent = $this->exportService->bookToContainedHtml($book);
return response()->make($htmlContent, 200, [ return $this->downloadResponse($htmlContent, $bookSlug . '.html');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
]);
} }
/** /**
@ -328,10 +322,7 @@ class BookController extends Controller
public function exportPlainText($bookSlug) public function exportPlainText($bookSlug)
{ {
$book = $this->entityRepo->getBySlug('book', $bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToPlainText($book); $textContent = $this->exportService->bookToPlainText($book);
return response()->make($htmlContent, 200, [ return $this->downloadResponse($textContent, $bookSlug . '.txt');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
]);
} }
} }

View File

@ -0,0 +1,242 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
class BookshelfController extends Controller
{
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Display a listing of the book.
* @return Response
*/
public function index()
{
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$this->setPageTitle(trans('entities.shelves'));
return view('shelves/index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'shelvesViewType' => $shelvesViewType
]);
}
/**
* Show the form for creating a new bookshelf.
* @return Response
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
Activity::add($bookshelf, 'bookshelf_create');
return redirect($bookshelf->getUrl());
}
/**
* Display the specified bookshelf.
* @param String $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
]);
}
/**
* Show the form for editing the specified bookshelf.
* @param $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function edit(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id);
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
'shelf' => $bookshelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
}
/**
* Update the specified bookshelf in storage.
* @param Request $request
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
}
/**
* Shows the page to confirm deletion
* @param $slug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showDelete(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
}
/**
* Remove the specified bookshelf from storage.
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function destroy(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
return redirect('/shelves');
}
/**
* Show the Restrictions view.
* @param $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
'shelf' => $bookshelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function restrict(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($bookshelf->getUrl());
}
/**
* Copy the permissions of a bookshelf to the child books.
* @param string $slug
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function copyPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($bookshelf->getUrl());
}
}

View File

@ -1,9 +1,9 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\EntityRepo; use BookStack\Auth\UserRepo;
use BookStack\Repos\UserRepo; use BookStack\Entities\Repos\EntityRepo;
use BookStack\Services\ExportService; use BookStack\Entities\ExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
@ -19,7 +19,7 @@ class ChapterController extends Controller
* ChapterController constructor. * ChapterController constructor.
* @param EntityRepo $entityRepo * @param EntityRepo $entityRepo
* @param UserRepo $userRepo * @param UserRepo $userRepo
* @param ExportService $exportService * @param \BookStack\Entities\ExportService $exportService
*/ */
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService) public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{ {
@ -250,10 +250,7 @@ class ChapterController extends Controller
{ {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter); $pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [ return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
} }
/** /**
@ -266,10 +263,7 @@ class ChapterController extends Controller
{ {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter); $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [ return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
} }
/** /**
@ -281,10 +275,7 @@ class ChapterController extends Controller
public function exportPlainText($bookSlug, $chapterSlug) public function exportPlainText($bookSlug, $chapterSlug)
{ {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter); $chapterText = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [ return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
} }
} }

View File

@ -1,8 +1,8 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\CommentRepo; use BookStack\Actions\CommentRepo;
use BookStack\Repos\EntityRepo; use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -13,8 +13,8 @@ class CommentController extends Controller
/** /**
* CommentController constructor. * CommentController constructor.
* @param EntityRepo $entityRepo * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param CommentRepo $commentRepo * @param \BookStack\Actions\CommentRepo $commentRepo
*/ */
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo) public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
{ {

View File

@ -2,13 +2,13 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use BookStack\Auth\User;
use BookStack\Ownable; use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use BookStack\User;
abstract class Controller extends BaseController abstract class Controller extends BaseController
{ {
@ -136,7 +136,6 @@ abstract class Controller extends BaseController
/** /**
* Create the response for when a request fails validation. * Create the response for when a request fails validation.
*
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param array $errors * @param array $errors
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
@ -151,4 +150,18 @@ abstract class Controller extends BaseController
->withInput($request->input()) ->withInput($request->input())
->withErrors($errors, $this->errorBag()); ->withErrors($errors, $this->errorBag());
} }
/**
* Create a response that forces a download in the browser.
* @param string $content
* @param string $fileName
* @return \Illuminate\Http\Response
*/
protected function downloadResponse(string $content, string $fileName)
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
} }

View File

@ -1,8 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\EntityRepo; use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
@ -33,42 +32,42 @@ class HomeController extends Controller
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor); $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$customHomepage = false; $homepageOption = setting('app-homepage-type', 'default');
$books = false; if (!in_array($homepageOption, $homepageOptions)) {
$booksViewType = false; $homepageOption = 'default';
// Check book homepage
$bookHomepageSetting = setting('app-book-homepage');
if ($bookHomepageSetting) {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
} else {
// Check custom homepage
$homepageSetting = setting('app-homepage');
if ($homepageSetting) {
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
}
} }
$view = 'home'; $commonData = [
if ($bookHomepageSetting) {
$view = 'home-book';
} else if ($customHomepage) {
$view = 'home-custom';
}
return view('common/' . $view, [
'activity' => $activity, 'activity' => $activity,
'recents' => $recents, 'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages, 'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages, 'draftPages' => $draftPages,
'customHomepage' => $customHomepage, ];
'books' => $books,
'booksViewType' => $booksViewType if ($homepageOption === 'bookshelves') {
]); $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
return view('common.home-book', $data);
}
if ($homepageOption === 'page') {
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
}
return view('common.home', $commonData);
} }
/** /**
@ -80,6 +79,7 @@ class HomeController extends Controller
{ {
$locale = app()->getLocale(); $locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale; $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') { if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey); $resp = cache($cacheKey);
} else { } else {
@ -90,15 +90,6 @@ class HomeController extends Controller
'entities' => trans('entities'), 'entities' => trans('entities'),
'errors' => trans('errors') 'errors' => trans('errors')
]; ];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], 'en'),
'components' => trans('components', [], 'en'),
'entities' => trans('entities', [], 'en'),
'errors' => trans('errors', [], 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}
$resp = 'window.translations = ' . json_encode($translations); $resp = 'window.translations = ' . json_encode($translations);
cache()->put($cacheKey, $resp, 120); cache()->put($cacheKey, $resp, 120);
} }

View File

@ -1,13 +1,12 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException; use BookStack\Repos\PageRepo;
use BookStack\Repos\EntityRepo; use BookStack\Uploads\Image;
use BookStack\Repos\ImageRepo; use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File; use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Image;
use BookStack\Repos\PageRepo;
class ImageController extends Controller class ImageController extends Controller
{ {
@ -220,7 +219,7 @@ class ImageController extends Controller
/** /**
* Show the usage of an image on pages. * Show the usage of an image on pages.
* @param EntityRepo $entityRepo * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id * @param $id
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */

View File

@ -1,32 +1,32 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo; use GatherContent\Htmldiff\Htmldiff;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
use GatherContent\Htmldiff\Htmldiff;
class PageController extends Controller class PageController extends Controller
{ {
protected $entityRepo; protected $pageRepo;
protected $exportService; protected $exportService;
protected $userRepo; protected $userRepo;
/** /**
* PageController constructor. * PageController constructor.
* @param EntityRepo $entityRepo * @param \BookStack\Entities\Repos\PageRepo $pageRepo
* @param ExportService $exportService * @param \BookStack\Entities\ExportService $exportService
* @param UserRepo $userRepo * @param UserRepo $userRepo
*/ */
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo) public function __construct(PageRepo $pageRepo, ExportService $exportService, UserRepo $userRepo)
{ {
$this->entityRepo = $entityRepo; $this->pageRepo = $pageRepo;
$this->exportService = $exportService; $this->exportService = $exportService;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
@ -38,21 +38,28 @@ class PageController extends Controller
* @param string $chapterSlug * @param string $chapterSlug
* @return Response * @return Response
* @internal param bool $pageSlug * @internal param bool $pageSlug
* @throws NotFoundException
*/ */
public function create($bookSlug, $chapterSlug = null) public function create($bookSlug, $chapterSlug = null)
{ {
$book = $this->entityRepo->getBySlug('book', $bookSlug); if ($chapterSlug !== null) {
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null; $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->pageRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book; $parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in // Redirect to draft edit screen if signed in
if ($this->signedIn) { if ($this->signedIn) {
$draft = $this->entityRepo->getDraftPage($book, $chapter); $draft = $this->pageRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl()); return redirect($draft->getUrl());
} }
// Otherwise show edit view // Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new')); $this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]); return view('pages/guest-create', ['parent' => $parent]);
} }
@ -71,13 +78,19 @@ class PageController extends Controller
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$book = $this->entityRepo->getBySlug('book', $bookSlug); if ($chapterSlug !== null) {
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null; $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->pageRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book; $parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
$page = $this->entityRepo->getDraftPage($book, $chapter); $page = $this->pageRepo->getDraftPage($book, $chapter);
$this->entityRepo->publishPageDraft($page, [ $this->pageRepo->publishPageDraft($page, [
'name' => $request->get('name'), 'name' => $request->get('name'),
'html' => '' 'html' => ''
]); ]);
@ -92,8 +105,8 @@ class PageController extends Controller
*/ */
public function editDraft($bookSlug, $pageId) public function editDraft($bookSlug, $pageId)
{ {
$draft = $this->entityRepo->getById('page', $pageId, true); $draft = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book); $this->checkOwnablePermission('page-create', $draft->parent);
$this->setPageTitle(trans('entities.pages_edit_draft')); $this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn; $draftsEnabled = $this->signedIn;
@ -119,21 +132,19 @@ class PageController extends Controller
]); ]);
$input = $request->all(); $input = $request->all();
$book = $this->entityRepo->getBySlug('book', $bookSlug); $draftPage = $this->pageRepo->getById('page', $pageId, true);
$book = $draftPage->book;
$draftPage = $this->entityRepo->getById('page', $pageId, true); $parent = $draftPage->parent;
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) { if ($parent->isA('chapter')) {
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent); $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
} else { } else {
$input['priority'] = $this->entityRepo->getNewBookPriority($parent); $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
} }
$page = $this->entityRepo->publishPageDraft($draftPage, $input); $page = $this->pageRepo->publishPageDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id); Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
@ -150,9 +161,9 @@ class PageController extends Controller
public function show($bookSlug, $pageSlug) public function show($bookSlug, $pageSlug)
{ {
try { try {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
} catch (NotFoundException $e) { } catch (NotFoundException $e) {
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug); $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) { if ($page === null) {
throw $e; throw $e;
} }
@ -161,9 +172,9 @@ class PageController extends Controller
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
$page->html = $this->entityRepo->renderPage($page); $page->html = $this->pageRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book); $sidebarTree = $this->pageRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($page->html); $pageNav = $this->pageRepo->getPageNav($page->html);
// check if the comment's are enabled // check if the comment's are enabled
$commentsEnabled = !setting('app-disable-comments'); $commentsEnabled = !setting('app-disable-comments');
@ -189,7 +200,7 @@ class PageController extends Controller
*/ */
public function getPageAjax($pageId) public function getPageAjax($pageId)
{ {
$page = $this->entityRepo->getById('page', $pageId); $page = $this->pageRepo->getById('page', $pageId);
return response()->json($page); return response()->json($page);
} }
@ -198,28 +209,29 @@ class PageController extends Controller
* @param string $bookSlug * @param string $bookSlug
* @param string $pageSlug * @param string $pageSlug
* @return Response * @return Response
* @throws NotFoundException
*/ */
public function edit($bookSlug, $pageSlug) public function edit($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
$page->isDraft = false; $page->isDraft = false;
// Check for active editing // Check for active editing
$warnings = []; $warnings = [];
if ($this->entityRepo->isPageEditingActive($page, 60)) { if ($this->pageRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60); $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
} }
// Check for a current draft version for this user // Check for a current draft version for this user
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id); if ($userPageDraft !== null) {
$page->name = $draft->name; $page->name = $userPageDraft->name;
$page->html = $draft->html; $page->html = $userPageDraft->html;
$page->markdown = $draft->markdown; $page->markdown = $userPageDraft->markdown;
$page->isDraft = true; $page->isDraft = true;
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft); $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
} }
if (count($warnings) > 0) { if (count($warnings) > 0) {
@ -247,9 +259,9 @@ class PageController extends Controller
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->entityRepo->updatePage($page, $page->book->id, $request->all()); $this->pageRepo->updatePage($page, $page->book->id, $request->all());
Activity::add($page, 'page_update', $page->book->id); Activity::add($page, 'page_update', $page->book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -262,7 +274,7 @@ class PageController extends Controller
*/ */
public function saveDraft(Request $request, $pageId) public function saveDraft(Request $request, $pageId)
{ {
$page = $this->entityRepo->getById('page', $pageId, true); $page = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
if (!$this->signedIn) { if (!$this->signedIn) {
@ -272,7 +284,7 @@ class PageController extends Controller
], 500); ], 500);
} }
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown'])); $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp; $updateTime = $draft->updated_at->timestamp;
return response()->json([ return response()->json([
@ -290,7 +302,7 @@ class PageController extends Controller
*/ */
public function redirectFromLink($pageId) public function redirectFromLink($pageId)
{ {
$page = $this->entityRepo->getById('page', $pageId); $page = $this->pageRepo->getById('page', $pageId);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -302,7 +314,7 @@ class PageController extends Controller
*/ */
public function showDelete($bookSlug, $pageSlug) public function showDelete($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]); return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
@ -318,7 +330,7 @@ class PageController extends Controller
*/ */
public function showDeleteDraft($bookSlug, $pageId) public function showDeleteDraft($bookSlug, $pageId)
{ {
$page = $this->entityRepo->getById('page', $pageId, true); $page = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]); return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
@ -333,10 +345,10 @@ class PageController extends Controller
*/ */
public function destroy($bookSlug, $pageSlug) public function destroy($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$book = $page->book; $book = $page->book;
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
$this->entityRepo->destroyPage($page); $this->pageRepo->destroyPage($page);
Activity::addMessage('page_delete', $book->id, $page->name); Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', trans('entities.pages_delete_success')); session()->flash('success', trans('entities.pages_delete_success'));
@ -352,11 +364,11 @@ class PageController extends Controller
*/ */
public function destroyDraft($bookSlug, $pageId) public function destroyDraft($bookSlug, $pageId)
{ {
$page = $this->entityRepo->getById('page', $pageId, true); $page = $this->pageRepo->getById('page', $pageId, true);
$book = $page->book; $book = $page->book;
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
session()->flash('success', trans('entities.pages_delete_draft_success')); session()->flash('success', trans('entities.pages_delete_draft_success'));
$this->entityRepo->destroyPage($page); $this->pageRepo->destroyPage($page);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -368,7 +380,7 @@ class PageController extends Controller
*/ */
public function showRevisions($bookSlug, $pageSlug) public function showRevisions($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
} }
@ -382,7 +394,7 @@ class PageController extends Controller
*/ */
public function showRevision($bookSlug, $pageSlug, $revisionId) public function showRevision($bookSlug, $pageSlug, $revisionId)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$revision = $page->revisions()->where('id', '=', $revisionId)->first(); $revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) { if ($revision === null) {
abort(404); abort(404);
@ -407,7 +419,7 @@ class PageController extends Controller
*/ */
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId) public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$revision = $page->revisions()->where('id', '=', $revisionId)->first(); $revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) { if ($revision === null) {
abort(404); abort(404);
@ -437,13 +449,47 @@ class PageController extends Controller
*/ */
public function restoreRevision($bookSlug, $pageSlug, $revisionId) public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId); $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
Activity::add($page, 'page_restore', $page->book->id); Activity::add($page, 'page_restore', $page->book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
/**
* Deletes a revision using the id of the specified revision.
* @param string $bookSlug
* @param string $pageSlug
* @param int $revId
* @throws NotFoundException
* @throws BadRequestException
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroyRevision($bookSlug, $pageSlug, $revId)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {
throw new NotFoundException("Revision #{$revId} not found");
}
// Get the current revision for the page
$currentRevision = $page->getCurrentRevision();
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
}
$revision->delete();
session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/** /**
* Exports a page to a PDF. * Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf * https://github.com/barryvdh/laravel-dompdf
@ -453,13 +499,10 @@ class PageController extends Controller
*/ */
public function exportPdf($bookSlug, $pageSlug) public function exportPdf($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page); $page->html = $this->pageRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page); $pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [ return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
} }
/** /**
@ -470,13 +513,10 @@ class PageController extends Controller
*/ */
public function exportHtml($bookSlug, $pageSlug) public function exportHtml($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page); $page->html = $this->pageRepo->renderPage($page);
$containedHtml = $this->exportService->pageToContainedHtml($page); $containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [ return $this->downloadResponse($containedHtml, $pageSlug . '.html');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
} }
/** /**
@ -487,12 +527,9 @@ class PageController extends Controller
*/ */
public function exportPlainText($bookSlug, $pageSlug) public function exportPlainText($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToPlainText($page); $pageText = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [ return $this->downloadResponse($pageText, $pageSlug . '.txt');
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
} }
/** /**
@ -501,7 +538,7 @@ class PageController extends Controller
*/ */
public function showRecentlyCreated() public function showRecentlyCreated()
{ {
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created')); $pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [ return view('pages/detailed-listing', [
'title' => trans('entities.recently_created_pages'), 'title' => trans('entities.recently_created_pages'),
'pages' => $pages 'pages' => $pages
@ -514,7 +551,7 @@ class PageController extends Controller
*/ */
public function showRecentlyUpdated() public function showRecentlyUpdated()
{ {
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated')); $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [ return view('pages/detailed-listing', [
'title' => trans('entities.recently_updated_pages'), 'title' => trans('entities.recently_updated_pages'),
'pages' => $pages 'pages' => $pages
@ -529,7 +566,7 @@ class PageController extends Controller
*/ */
public function showRestrict($bookSlug, $pageSlug) public function showRestrict($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page); $this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles(); $roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [ return view('pages/restrictions', [
@ -547,7 +584,7 @@ class PageController extends Controller
*/ */
public function showMove($bookSlug, $pageSlug) public function showMove($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
return view('pages/move', [ return view('pages/move', [
'book' => $page->book, 'book' => $page->book,
@ -565,7 +602,7 @@ class PageController extends Controller
*/ */
public function move($bookSlug, $pageSlug, Request $request) public function move($bookSlug, $pageSlug, Request $request)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null); $entitySelection = $request->get('entity_selection', null);
@ -579,7 +616,7 @@ class PageController extends Controller
try { try {
$parent = $this->entityRepo->getById($entityType, $entityId); $parent = $this->pageRepo->getById($entityType, $entityId);
} catch (\Exception $e) { } catch (\Exception $e) {
session()->flash(trans('entities.selected_book_chapter_not_found')); session()->flash(trans('entities.selected_book_chapter_not_found'));
return redirect()->back(); return redirect()->back();
@ -587,7 +624,7 @@ class PageController extends Controller
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
$this->entityRepo->changePageParent($page, $parent); $this->pageRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id); Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name])); session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
@ -603,7 +640,7 @@ class PageController extends Controller
*/ */
public function showCopy($bookSlug, $pageSlug) public function showCopy($bookSlug, $pageSlug)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
session()->flashInput(['name' => $page->name]); session()->flashInput(['name' => $page->name]);
return view('pages/copy', [ return view('pages/copy', [
@ -622,7 +659,7 @@ class PageController extends Controller
*/ */
public function copy($bookSlug, $pageSlug, Request $request) public function copy($bookSlug, $pageSlug, Request $request)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null); $entitySelection = $request->get('entity_selection', null);
@ -634,7 +671,7 @@ class PageController extends Controller
$entityId = intval($stringExploded[1]); $entityId = intval($stringExploded[1]);
try { try {
$parent = $this->entityRepo->getById($entityType, $entityId); $parent = $this->pageRepo->getById($entityType, $entityId);
} catch (\Exception $e) { } catch (\Exception $e) {
session()->flash(trans('entities.selected_book_chapter_not_found')); session()->flash(trans('entities.selected_book_chapter_not_found'));
return redirect()->back(); return redirect()->back();
@ -643,7 +680,7 @@ class PageController extends Controller
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
$pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', '')); $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
Activity::add($pageCopy, 'page_create', $pageCopy->book->id); Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
session()->flash('success', trans('entities.pages_copy_success')); session()->flash('success', trans('entities.pages_copy_success'));
@ -661,9 +698,9 @@ class PageController extends Controller
*/ */
public function restrict($bookSlug, $pageSlug, Request $request) public function restrict($bookSlug, $pageSlug, Request $request)
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page); $this->checkOwnablePermission('restrictions-manage', $page);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page); $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
session()->flash('success', trans('entities.pages_permissions_success')); session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl()); return redirect($page->getUrl());
} }

View File

@ -1,7 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class PermissionController extends Controller class PermissionController extends Controller
@ -11,7 +11,7 @@ class PermissionController extends Controller
/** /**
* PermissionController constructor. * PermissionController constructor.
* @param PermissionsRepo $permissionsRepo * @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
*/ */
public function __construct(PermissionsRepo $permissionsRepo) public function __construct(PermissionsRepo $permissionsRepo)
{ {
@ -78,6 +78,7 @@ class PermissionController extends Controller
* @param $id * @param $id
* @param Request $request * @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
*/ */
public function updateRole($id, Request $request) public function updateRole($id, Request $request)
{ {

View File

@ -1,8 +1,8 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo; use BookStack\Actions\ViewService;
use BookStack\Services\SearchService; use BookStack\Entities\Repos\EntityRepo;
use BookStack\Services\ViewService; use BookStack\Entities\SearchService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class SearchController extends Controller class SearchController extends Controller
@ -13,7 +13,7 @@ class SearchController extends Controller
/** /**
* SearchController constructor. * SearchController constructor.
* @param EntityRepo $entityRepo * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param ViewService $viewService * @param ViewService $viewService
* @param SearchService $searchService * @param SearchService $searchService
*/ */
@ -97,7 +97,7 @@ class SearchController extends Controller
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results']; $entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else { } else {
$entityNames = $entityTypes->map(function ($type) { $entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type); return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy
})->toArray(); })->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission); $entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
} }

View File

@ -1,6 +1,6 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Services\ImageService; use BookStack\Uploads\ImageService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Setting; use Setting;

View File

@ -1,6 +1,6 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Repos\TagRepo; use BookStack\Actions\TagRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class TagController extends Controller class TagController extends Controller

View File

@ -1,11 +1,11 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Exception; use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use BookStack\User;
class UserController extends Controller class UserController extends Controller
{ {
@ -60,6 +60,7 @@ class UserController extends Controller
* Store a newly created user in storage. * Store a newly created user in storage.
* @param Request $request * @param Request $request
* @return Response * @return Response
* @throws UserUpdateException
*/ */
public function store(Request $request) public function store(Request $request)
{ {
@ -90,10 +91,10 @@ class UserController extends Controller
if ($request->filled('roles')) { if ($request->filled('roles')) {
$roles = $request->get('roles'); $roles = $request->get('roles');
$user->roles()->sync($roles); $this->userRepo->setUserRoles($user, $roles);
} }
$this->userRepo->downloadGravatarToUserAvatar($user); $this->userRepo->downloadAndAssignUserAvatar($user);
return redirect('/settings/users'); return redirect('/settings/users');
} }
@ -101,7 +102,7 @@ class UserController extends Controller
/** /**
* Show the form for editing the specified user. * Show the form for editing the specified user.
* @param int $id * @param int $id
* @param SocialAuthService $socialAuthService * @param \BookStack\Auth\Access\SocialAuthService $socialAuthService
* @return Response * @return Response
*/ */
public function edit($id, SocialAuthService $socialAuthService) public function edit($id, SocialAuthService $socialAuthService)
@ -123,8 +124,9 @@ class UserController extends Controller
/** /**
* Update the specified user in storage. * Update the specified user in storage.
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return Response * @return Response
* @throws UserUpdateException
*/ */
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
@ -141,13 +143,13 @@ class UserController extends Controller
'setting' => 'array' 'setting' => 'array'
]); ]);
$user = $this->user->findOrFail($id); $user = $this->userRepo->getById($id);
$user->fill($request->all()); $user->fill($request->all());
// Role updates // Role updates
if (userCan('users-manage') && $request->filled('roles')) { if (userCan('users-manage') && $request->filled('roles')) {
$roles = $request->get('roles'); $roles = $request->get('roles');
$user->roles()->sync($roles); $this->userRepo->setUserRoles($user, $roles);
} }
// Password updates // Password updates
@ -186,7 +188,7 @@ class UserController extends Controller
return $this->currentUser->id == $id; return $this->currentUser->id == $id;
}); });
$user = $this->user->findOrFail($id); $user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]); return view('users/delete', ['user' => $user]);
} }
@ -195,6 +197,7 @@ class UserController extends Controller
* Remove the specified user from storage. * Remove the specified user from storage.
* @param int $id * @param int $id
* @return Response * @return Response
* @throws \Exception
*/ */
public function destroy($id) public function destroy($id)
{ {
@ -252,7 +255,7 @@ class UserController extends Controller
return $this->currentUser->id == $id; return $this->currentUser->id == $id;
}); });
$viewType = $request->get('book_view_type'); $viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) { if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list'; $viewType = 'list';
} }
@ -262,4 +265,27 @@ class UserController extends Controller
return redirect()->back(302, [], "/settings/users/$id"); return redirect()->back(302, [], "/settings/users/$id");
} }
/**
* Update the user's preferred shelf-list display setting.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function switchShelfView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->userRepo->getById($id);
setting()->putUser($user, 'bookshelves_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
}
} }

View File

@ -2,9 +2,13 @@
use Carbon\Carbon; use Carbon\Carbon;
use Closure; use Closure;
use Illuminate\Http\Request;
class Localization class Localization
{ {
protected $rtlLocales = ['ar'];
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
@ -15,21 +19,38 @@ class Localization
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
$defaultLang = config('app.locale'); $defaultLang = config('app.locale');
if (user()->isDefault()) {
$locale = $defaultLang; if (user()->isDefault() && config('app.auto_detect_locale')) {
$availableLocales = config('app.locales'); $locale = $this->autoDetectLocale($request, $defaultLang);
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) {
continue;
}
$locale = $lang;
break;
}
} else { } else {
$locale = setting()->getUser(user(), 'language', $defaultLang); $locale = setting()->getUser(user(), 'language', $defaultLang);
} }
// Set text direction
if (in_array($locale, $this->rtlLocales)) {
config()->set('app.rtl', true);
}
app()->setLocale($locale); app()->setLocale($locale);
Carbon::setLocale($locale); Carbon::setLocale($locale);
return $next($request); return $next($request);
} }
/**
* Autodetect the visitors locale by matching locales in their headers
* against the locales supported by BookStack.
* @param Request $request
* @param string $default
* @return string
*/
protected function autoDetectLocale(Request $request, string $default)
{
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (in_array($lang, $availableLocales)) {
return $lang;
}
}
return $default;
}
} }

View File

@ -3,8 +3,8 @@
namespace BookStack\Http\Middleware; namespace BookStack\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware; use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware class TrustProxies extends Middleware
{ {

View File

@ -1,17 +1,7 @@
<?php <?php namespace BookStack\Notifications;
namespace BookStack\Notifications; class ConfirmEmail extends MailNotification
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification implements ShouldQueue
{ {
use Queueable;
public $token; public $token;
/** /**
@ -23,17 +13,6 @@ class ConfirmEmail extends Notification implements ShouldQueue
$this->token = $token; $this->token = $token;
} }
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.
* *
@ -43,10 +22,10 @@ class ConfirmEmail extends Notification implements ShouldQueue
public function toMail($notifiable) public function toMail($notifiable)
{ {
$appName = ['appName' => setting('app-name')]; $appName = ['appName' => setting('app-name')];
return (new MailMessage) return $this->newMailMessage()
->subject(trans('auth.email_confirm_subject', $appName)) ->subject(trans('auth.email_confirm_subject', $appName))
->greeting(trans('auth.email_confirm_greeting', $appName)) ->greeting(trans('auth.email_confirm_greeting', $appName))
->line(trans('auth.email_confirm_text')) ->line(trans('auth.email_confirm_text'))
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token)); ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
} }
} }

View File

@ -0,0 +1,35 @@
<?php namespace BookStack\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MailNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Create a new mail message.
* @return MailMessage
*/
protected function newMailMessage()
{
return (new MailMessage)->view([
'html' => 'vendor.notifications.email',
'text' => 'vendor.notifications.email-plain'
]);
}
}

View File

@ -1,11 +1,7 @@
<?php <?php namespace BookStack\Notifications;
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification; class ResetPassword extends MailNotification
use Illuminate\Notifications\Messages\MailMessage;
class ResetPassword extends Notification
{ {
/** /**
* The password reset token. * The password reset token.
@ -24,17 +20,6 @@ class ResetPassword extends Notification
$this->token = $token; $this->token = $token;
} }
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/** /**
* Build the mail representation of the notification. * Build the mail representation of the notification.
* *
@ -42,7 +27,7 @@ class ResetPassword extends Notification
*/ */
public function toMail() public function toMail()
{ {
return (new MailMessage) return $this->newMailMessage()
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')])) ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
->line(trans('auth.email_reset_text')) ->line(trans('auth.email_reset_text'))
->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token)) ->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))

View File

@ -1,5 +1,7 @@
<?php namespace BookStack; <?php namespace BookStack;
use BookStack\Auth\User;
abstract class Ownable extends Model abstract class Ownable extends Model
{ {
/** /**

View File

@ -1,8 +1,15 @@
<?php namespace BookStack\Providers; <?php namespace BookStack\Providers;
use BookStack\Services\SettingService; use Blade;
use BookStack\Setting; use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Schema;
use Validator; use Validator;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -20,12 +27,21 @@ class AppServiceProvider extends ServiceProvider
return in_array($value->getMimeType(), $imageMimes); return in_array($value->getMimeType(), $imageMimes);
}); });
\Blade::directive('icon', function ($expression) { // Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>"; return "<?php echo icon($expression); ?>";
}); });
// Allow longer string lengths after upgrade to utf8mb4 // Allow longer string lengths after upgrade to utf8mb4
\Schema::defaultStringLength(191); Schema::defaultStringLength(191);
// Set morph-map due to namespace changes
Relation::morphMap([
'BookStack\\Bookshelf' => Bookshelf::class,
'BookStack\\Book' => Book::class,
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
]);
} }
/** /**

View File

@ -3,7 +3,7 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use Auth; use Auth;
use BookStack\Services\LdapService; use BookStack\Auth\Access\LdapService;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider

View File

@ -3,7 +3,6 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider class BroadcastServiceProvider extends ServiceProvider
{ {

View File

@ -2,18 +2,19 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use BookStack\Activity; use BookStack\Actions\Activity;
use BookStack\Image; use BookStack\Actions\ActivityService;
use BookStack\Services\ImageService; use BookStack\Actions\View;
use BookStack\Services\PermissionService; use BookStack\Actions\ViewService;
use BookStack\Services\ViewService; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Setting; use BookStack\Settings\Setting;
use BookStack\View; use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Factory; use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use BookStack\Services\ActivityService;
use BookStack\Services\SettingService;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
class CustomFacadeProvider extends ServiceProvider class CustomFacadeProvider extends ServiceProvider
@ -61,7 +62,8 @@ class CustomFacadeProvider extends ServiceProvider
$this->app->make(Image::class), $this->app->make(Image::class),
$this->app->make(ImageManager::class), $this->app->make(ImageManager::class),
$this->app->make(Factory::class), $this->app->make(Factory::class),
$this->app->make(Repository::class) $this->app->make(Repository::class),
$this->app->make(HttpFetcher::class)
); );
}); });
} }

View File

@ -2,7 +2,6 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Manager\SocialiteWasCalled; use SocialiteProviders\Manager\SocialiteWasCalled;
@ -20,6 +19,7 @@ class EventServiceProvider extends ServiceProvider
'SocialiteProviders\Okta\OktaExtendSocialite@handle', 'SocialiteProviders\Okta\OktaExtendSocialite@handle',
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle', 'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle', 'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
], ],
]; ];

View File

@ -2,9 +2,7 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use BookStack\Role; use BookStack\Auth\Access\LdapService;
use BookStack\Services\LdapService;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Auth\UserProvider;
@ -19,7 +17,7 @@ class LdapUserProvider implements UserProvider
protected $model; protected $model;
/** /**
* @var LdapService * @var \BookStack\Auth\LdapService
*/ */
protected $ldapService; protected $ldapService;
@ -27,7 +25,7 @@ class LdapUserProvider implements UserProvider
/** /**
* LdapUserProvider constructor. * LdapUserProvider constructor.
* @param $model * @param $model
* @param LdapService $ldapService * @param \BookStack\Auth\LdapService $ldapService
*/ */
public function __construct($model, LdapService $ldapService) public function __construct($model, LdapService $ldapService)
{ {

View File

@ -2,7 +2,6 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Route; use Route;

View File

@ -0,0 +1,32 @@
<?php namespace BookStack\Providers;
use BookStack\Translation\Translator;
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
}

View File

@ -1,165 +0,0 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
/**
* Class LdapService
* Handles any app-specific LDAP tasks.
* @package BookStack\Services
*/
class LdapService
{
protected $ldap;
protected $ldapConnection;
protected $config;
/**
* LdapService constructor.
* @param Ldap $ldap
*/
public function __construct(Ldap $ldap)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
* @param $userName
* @return array|null
* @throws LdapException
*/
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) {
return null;
}
$user = $users[0];
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
public function validateUserCredentials(Authenticatable $user, $username, $password)
{
$ldapUser = $this->getUserDetails($username);
if ($ldapUser === null) {
return false;
}
if ($ldapUser['uid'] !== $user->external_auth_id) {
return false;
}
$ldapConnection = $this->getConnection();
try {
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
} catch (\ErrorException $e) {
$ldapBind = false;
}
return $ldapBind;
}
/**
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
* @param $connection
* @throws LdapException
*/
protected function bindSystemUser($connection)
{
$ldapDn = $this->config['dn'];
$ldapPass = $this->config['pass'];
$isAnonymous = ($ldapDn === false || $ldapPass === false);
if ($isAnonymous) {
$ldapBind = $this->ldap->bind($connection);
} else {
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) {
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
}
}
/**
* Get the connection to the LDAP server.
* Creates a new connection if one does not exist.
* @return resource
* @throws LdapException
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) {
return $this->ldapConnection;
}
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) {
array_unshift($ldapServer, '');
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
}
// Set any required options
if ($this->config['version']) {
$this->ldap->setVersion($ldapConnection, $this->config['version']);
}
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
* @param array $attrs
* @return string
*/
protected function buildFilter($filterString, array $attrs)
{
$newAttrs = [];
foreach ($attrs as $key => $attrText) {
$newKey = '${' . $key . '}';
$newAttrs[$newKey] = $attrText;
}
return strtr($filterString, $newAttrs);
}
}

View File

@ -1,4 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Settings;
use BookStack\Model;
class Setting extends Model class Setting extends Model
{ {

View File

@ -1,7 +1,5 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Settings;
use BookStack\Setting;
use BookStack\User;
use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Cache\Repository as Cache;
/** /**
@ -55,7 +53,7 @@ class SettingService
/** /**
* Get a user-specific setting from the database or cache. * Get a user-specific setting from the database or cache.
* @param User $user * @param \BookStack\Auth\User $user
* @param $key * @param $key
* @param bool $default * @param bool $default
* @return bool|string * @return bool|string
@ -174,7 +172,7 @@ class SettingService
/** /**
* Put a user-specific setting into the database. * Put a user-specific setting into the database.
* @param User $user * @param \BookStack\Auth\User $user
* @param $key * @param $key
* @param $value * @param $value
* @return bool * @return bool

View File

@ -0,0 +1,74 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
/**
* Mapping of locales to their base locales
* @var array
*/
protected $baseLocaleMap = [
'de_informal' => 'de',
];
/**
* Get the translation for a given key.
*
* @param string $key
* @param array $replace
* @param string $locale
* @return string|array|null
*/
public function trans($key, array $replace = [], $locale = null)
{
$translation = $this->get($key, $replace, $locale);
if (is_array($translation)) {
$translation = $this->mergeBackupTranslations($translation, $key, $locale);
}
return $translation;
}
/**
* Merge the fallback translations, and base translations if existing,
* into the provided core key => value array of translations content.
* @param array $translationArray
* @param string $key
* @param null $locale
* @return array
*/
protected function mergeBackupTranslations(array $translationArray, string $key, $locale = null)
{
$fallback = $this->get($key, [], $this->fallback);
$baseLocale = $this->getBaseLocale($locale ?? $this->locale);
$baseTranslations = $baseLocale ? $this->get($key, [], $baseLocale) : [];
return array_replace_recursive($fallback, $baseTranslations, $translationArray);
}
/**
* Get the array of locales to be checked.
*
* @param string|null $locale
* @return array
*/
protected function localeArray($locale)
{
$primaryLocale = $locale ?: $this->locale;
return array_filter([$primaryLocale, $this->getBaseLocale($primaryLocale), $this->fallback]);
}
/**
* Get the locale to extend for the given locale.
*
* @param string $locale
* @return string|null
*/
protected function getBaseLocale($locale)
{
return $this->baseLocaleMap[$locale] ?? null;
}
}

View File

@ -1,4 +1,7 @@
<?php namespace BookStack; <?php namespace BookStack\Uploads;
use BookStack\Entities\Page;
use BookStack\Ownable;
class Attachment extends Ownable class Attachment extends Ownable
{ {
@ -18,7 +21,7 @@ class Attachment extends Ownable
/** /**
* Get the page this file was uploaded to. * Get the page this file was uploaded to.
* @return Page * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
public function page() public function page()
{ {

View File

@ -1,7 +1,6 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Uploads;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use Exception; use Exception;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;

View File

@ -0,0 +1,34 @@
<?php namespace BookStack\Uploads;
use BookStack\Exceptions\HttpFetchException;
class HttpFetcher
{
/**
* Fetch content from an external URI.
* @param string $uri
* @return bool|string
* @throws HttpFetchException
*/
public function fetch(string $uri)
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $uri,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_CONNECTTIMEOUT => 5
]);
$data = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new HttpFetchException($err);
}
return $data;
}
}

View File

@ -1,5 +1,6 @@
<?php namespace BookStack; <?php namespace BookStack\Uploads;
use BookStack\Ownable;
use Images; use Images;
class Image extends Ownable class Image extends Ownable
@ -19,5 +20,4 @@ class Image extends Ownable
{ {
return Images::getThumbnail($this, $width, $height, $keepRatio); return Images::getThumbnail($this, $width, $height, $keepRatio);
} }
} }

View File

@ -1,9 +1,7 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Uploads;
use BookStack\Image; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Page; use BookStack\Entities\Page;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo class ImageRepo
@ -18,8 +16,8 @@ class ImageRepo
* ImageRepo constructor. * ImageRepo constructor.
* @param Image $image * @param Image $image
* @param ImageService $imageService * @param ImageService $imageService
* @param PermissionService $permissionService * @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @param Page $page * @param \BookStack\Entities\Page $page
*/ */
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page) public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
{ {

View File

@ -1,14 +1,14 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Uploads;
use BookStack\Auth\User;
use BookStack\Exceptions\HttpFetchException;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use DB; use DB;
use Exception; use Exception;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Cache\Repository as Cache;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService extends UploadService class ImageService extends UploadService
@ -18,6 +18,7 @@ class ImageService extends UploadService
protected $cache; protected $cache;
protected $storageUrl; protected $storageUrl;
protected $image; protected $image;
protected $http;
/** /**
* ImageService constructor. * ImageService constructor.
@ -25,12 +26,14 @@ class ImageService extends UploadService
* @param ImageManager $imageTool * @param ImageManager $imageTool
* @param FileSystem $fileSystem * @param FileSystem $fileSystem
* @param Cache $cache * @param Cache $cache
* @param HttpFetcher $http
*/ */
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache) public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
{ {
$this->image = $image; $this->image = $image;
$this->imageTool = $imageTool; $this->imageTool = $imageTool;
$this->cache = $cache; $this->cache = $cache;
$this->http = $http;
parent::__construct($fileSystem); parent::__construct($fileSystem);
} }
@ -96,8 +99,9 @@ class ImageService extends UploadService
private function saveNewFromUrl($url, $type, $imageName = false) private function saveNewFromUrl($url, $type, $imageName = false)
{ {
$imageName = $imageName ? $imageName : basename($url); $imageName = $imageName ? $imageName : basename($url);
$imageData = file_get_contents($url); try {
if ($imageData === false) { $imageData = $this->http->fetch($url);
} catch (HttpFetchException $exception) {
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
} }
return $this->saveNew($imageName, $imageData, $type); return $this->saveNew($imageName, $imageData, $type);
@ -280,24 +284,57 @@ class ImageService extends UploadService
} }
/** /**
* Save a gravatar image and set a the profile image for a user. * Save an avatar image from an external service.
* @param User $user * @param \BookStack\Auth\User $user
* @param int $size * @param int $size
* @return mixed * @return Image
* @throws Exception * @throws Exception
*/ */
public function saveUserGravatar(User $user, $size = 500) public function saveUserAvatar(User $user, $size = 500)
{ {
$emailHash = md5(strtolower(trim($user->email))); $avatarUrl = $this->getAvatarUrl();
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; $email = strtolower(trim($user->email));
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
$image = $this->saveNewFromUrl($url, 'user', $imageName); $replacements = [
'${hash}' => md5($email),
'${size}' => $size,
'${email}' => urlencode($email),
];
$userAvatarUrl = strtr($avatarUrl, $replacements);
$imageName = str_replace(' ', '-', $user->name . '-avatar.png');
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
$image->created_by = $user->id; $image->created_by = $user->id;
$image->updated_by = $user->id; $image->updated_by = $user->id;
$image->save(); $image->save();
return $image; return $image;
} }
/**
* Check if fetching external avatars is enabled.
* @return bool
*/
public function avatarFetchEnabled()
{
$fetchUrl = $this->getAvatarUrl();
return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
}
/**
* Get the URL to fetch avatars from.
* @return string|mixed
*/
protected function getAvatarUrl()
{
$url = trim(config('services.avatar_url'));
if (empty($url) && !config('services.disable_services')) {
$url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
}
return $url;
}
/** /**
* Delete gallery and drawings that are not within HTML content of pages or page revisions. * Delete gallery and drawings that are not within HTML content of pages or page revisions.
@ -316,25 +353,25 @@ class ImageService extends UploadService
$deletedPaths = []; $deletedPaths = [];
$this->image->newQuery()->whereIn('type', $types) $this->image->newQuery()->whereIn('type', $types)
->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) { ->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
foreach ($images as $image) { foreach ($images as $image) {
$searchQuery = '%' . basename($image->path) . '%'; $searchQuery = '%' . basename($image->path) . '%';
$inPage = DB::table('pages') $inPage = DB::table('pages')
->where('html', 'like', $searchQuery)->count() > 0; ->where('html', 'like', $searchQuery)->count() > 0;
$inRevision = false; $inRevision = false;
if ($checkRevisions) { if ($checkRevisions) {
$inRevision = DB::table('page_revisions') $inRevision = DB::table('page_revisions')
->where('html', 'like', $searchQuery)->count() > 0; ->where('html', 'like', $searchQuery)->count() > 0;
} }
if (!$inPage && !$inRevision) { if (!$inPage && !$inRevision) {
$deletedPaths[] = $image->path; $deletedPaths[] = $image->path;
if (!$dryRun) { if (!$dryRun) {
$this->destroy($image); $this->destroy($image);
} }
} }
} }
}); });
return $deletedPaths; return $deletedPaths;
} }
@ -366,14 +403,7 @@ class ImageService extends UploadService
} }
} else { } else {
try { try {
$ch = curl_init(); $imageData = $this->http->fetch($uri);
curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
$imageData = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new \Exception("Image fetch failed, Received error: " . $err);
}
} catch (\Exception $e) { } catch (\Exception $e) {
} }
} }

View File

@ -1,9 +1,9 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Uploads;
use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
class UploadService abstract class UploadService
{ {
/** /**

View File

@ -30,11 +30,11 @@ function versioned_asset($file = '')
/** /**
* Helper method to get the current User. * Helper method to get the current User.
* Defaults to public 'Guest' user if not logged in. * Defaults to public 'Guest' user if not logged in.
* @return \BookStack\User * @return \BookStack\Auth\User
*/ */
function user() function user()
{ {
return auth()->user() ?: \BookStack\User::getDefault(); return auth()->user() ?: \BookStack\Auth\User::getDefault();
} }
/** /**
@ -61,7 +61,7 @@ function userCan($permission, Ownable $ownable = null)
} }
// Check permission on ownable item // Check permission on ownable item
$permissionService = app(\BookStack\Services\PermissionService::class); $permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
return $permissionService->checkOwnableUserAccess($ownable, $permission); return $permissionService->checkOwnableUserAccess($ownable, $permission);
} }
@ -69,11 +69,11 @@ function userCan($permission, Ownable $ownable = null)
* Helper to access system settings. * Helper to access system settings.
* @param $key * @param $key
* @param bool $default * @param bool $default
* @return bool|string|\BookStack\Services\SettingService * @return bool|string|\BookStack\Settings\SettingService
*/ */
function setting($key = null, $default = false) function setting($key = null, $default = false)
{ {
$settingService = resolve(\BookStack\Services\SettingService::class); $settingService = resolve(\BookStack\Settings\SettingService::class);
if (is_null($key)) { if (is_null($key)) {
return $settingService; return $settingService;
} }
@ -92,10 +92,15 @@ function baseUrl($path, $forceAppDomain = false)
if ($isFullUrl && !$forceAppDomain) { if ($isFullUrl && !$forceAppDomain) {
return $path; return $path;
} }
$path = trim($path, '/'); $path = trim($path, '/');
$base = rtrim(config('app.url'), '/');
// Remove non-specified domain if forced and we have a domain // Remove non-specified domain if forced and we have a domain
if ($isFullUrl && $forceAppDomain) { if ($isFullUrl && $forceAppDomain) {
if (!empty($base) && strpos($path, $base) === 0) {
$path = trim(substr($path, strlen($base) - 1));
}
$explodedPath = explode('/', $path); $explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3)); $path = implode('/', array_splice($explodedPath, 3));
} }
@ -105,7 +110,7 @@ function baseUrl($path, $forceAppDomain = false)
return url($path); return url($path);
} }
return rtrim(config('app.url'), '/') . '/' . $path; return $base . '/' . $path;
} }
/** /**

View File

@ -5,10 +5,16 @@
"license": "MIT", "license": "MIT",
"type": "project", "type": "project",
"require": { "require": {
"php": ">=7.0.0", "php": ">=7.0.5",
"laravel/framework": "~5.5.22", "ext-json": "*",
"fideloper/proxy": "~3.3",
"ext-tidy": "*", "ext-tidy": "*",
"ext-dom": "*",
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*",
"ext-curl": "*",
"laravel/framework": "~5.5.44",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4", "intervention/image": "^2.4",
"laravel/socialite": "^3.0", "laravel/socialite": "^3.0",
"league/flysystem-aws-s3-v3": "^1.0", "league/flysystem-aws-s3-v3": "^1.0",
@ -20,7 +26,9 @@
"socialiteproviders/microsoft-azure": "^3.0", "socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0", "socialiteproviders/okta": "^1.0",
"socialiteproviders/gitlab": "^3.0", "socialiteproviders/gitlab": "^3.0",
"socialiteproviders/twitch": "^3.0" "socialiteproviders/twitch": "^3.0",
"socialiteproviders/discord": "^2.0",
"doctrine/dbal": "^2.5"
}, },
"require-dev": { "require-dev": {
"filp/whoops": "~2.0", "filp/whoops": "~2.0",
@ -79,7 +87,7 @@
"optimize-autoloader": true, "optimize-autoloader": true,
"preferred-install": "dist", "preferred-install": "dist",
"platform": { "platform": {
"php": "7.0" "php": "7.0.5"
} }
} }
} }

1529
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,143 +1,84 @@
<?php <?php
/**
* Global app configuration options.
*
* Changes to these config files are not supported by BookStack and may break upon updates.
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
return [ return [
// The environment to run BookStack in.
// Options: production, development, demo, testing
'env' => env('APP_ENV', 'production'), 'env' => env('APP_ENV', 'production'),
/** // Enter the application in debug mode.
* Set the default view type for various lists. Can be overridden by user preferences. // Shows much more verbose error messages. Has potential to show
* This will be used for public viewers and users that have not set a preference. // private configuration variables so should remain disabled in public.
*/ 'debug' => env('APP_DEBUG', false),
// Set the default view type for various lists. Can be overridden by user preferences.
// These will be used for public viewers and users that have not set a preference.
'views' => [ 'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list') 'books' => env('APP_VIEWS_BOOKS', 'list')
], ],
/** // The number of revisions to keep in the database.
* Allow <script> tags to entered within page content. // Once this limit is reached older revisions will be deleted.
* <script> tags are escaped by default. // If set to false then a limit will not be enforced.
* Even when overridden the WYSIWYG editor may still escape script content. 'revision_limit' => env('REVISION_LIMIT', 50),
*/
// Allow <script> tags to entered within page content.
// <script> tags are escaped by default.
// Even when overridden the WYSIWYG editor may still escape script content.
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false), 'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
/** // Override the default behaviour for allowing crawlers to crawl the instance.
* Override the default behaviour for allowing crawlers to crawl the instance. // May be ignored if view has be overridden or modified.
* May be ignored if view has be overridden or modified. // Defaults to null since, if not set, 'app-public' status used instead.
* Defaults to null since, if not set, 'app-public' status used instead.
*/
'allow_robots' => env('ALLOW_ROBOTS', null), 'allow_robots' => env('ALLOW_ROBOTS', null),
/* // Application Base URL, Used by laravel in development commands
|-------------------------------------------------------------------------- // and used by BookStack in URL generation.
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''), 'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
/* // Application timezone for back-end date functions.
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC', 'timezone' => 'UTC',
/* // Default locale to use
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => env('APP_LANG', 'en'), 'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN', 'zh_TW'],
/* // Locales available
|-------------------------------------------------------------------------- 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
// Application Fallback Locale
'fallback_locale' => 'en', 'fallback_locale' => 'en',
/* // Enable right-to-left text control.
|-------------------------------------------------------------------------- 'rtl' => false,
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
// Auto-detect the locale for public users
// For public users their locale can be guessed by headers sent by their
// browser. This is usually set by users in their browser settings.
// If not found the default app locale will be used.
'auto_detect_locale' => env('APP_AUTO_LANG_PUBLIC', true),
// Encryption key
'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'),
// Encryption cipher
'cipher' => 'AES-256-CBC', 'cipher' => 'AES-256-CBC',
/* // Logging configuration
|-------------------------------------------------------------------------- // Options: single, daily, syslog, errorlog
| Logging Configuration
|--------------------------------------------------------------------------
|
| Here you may configure the log settings for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Settings: "single", "daily", "syslog", "errorlog"
|
*/
'log' => env('APP_LOGGING', 'single'), 'log' => env('APP_LOGGING', 'single'),
/* // Application Services Provides
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [ 'providers' => [
/* // Laravel Framework Service Providers...
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class, Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class, Illuminate\Bus\BusServiceProvider::class,
@ -155,25 +96,22 @@ return [
Illuminate\Redis\RedisServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class, Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class, Illuminate\View\ViewServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Notifications\NotificationServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class, SocialiteProviders\Manager\ServiceProvider::class,
/** // Third party service providers
* Third Party
*/
Intervention\Image\ImageServiceProvider::class, Intervention\Image\ImageServiceProvider::class,
Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\Snappy\ServiceProvider::class, Barryvdh\Snappy\ServiceProvider::class,
/* // BookStack replacement service providers (Extends Laravel)
* Application Service Providers...
*/
BookStack\Providers\PaginationServiceProvider::class, BookStack\Providers\PaginationServiceProvider::class,
BookStack\Providers\TranslationServiceProvider::class,
// BookStack custom service providers
BookStack\Providers\AuthServiceProvider::class, BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class, BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\BroadcastServiceProvider::class, BookStack\Providers\BroadcastServiceProvider::class,
@ -193,8 +131,10 @@ return [
| |
*/ */
// Class aliases, Registered on application start
'aliases' => [ 'aliases' => [
// Laravel
'App' => Illuminate\Support\Facades\App::class, 'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class, 'Auth' => Illuminate\Support\Facades\Auth::class,
@ -230,25 +170,20 @@ return [
'View' => Illuminate\Support\Facades\View::class, 'View' => Illuminate\Support\Facades\View::class,
'Socialite' => Laravel\Socialite\Facades\Socialite::class, 'Socialite' => Laravel\Socialite\Facades\Socialite::class,
/** // Third Party
* Third Party
*/
'ImageTool' => Intervention\Image\Facades\Image::class, 'ImageTool' => Intervention\Image\Facades\Image::class,
'DomPDF' => Barryvdh\DomPDF\Facade::class, 'DomPDF' => Barryvdh\DomPDF\Facade::class,
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class, 'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
/** // Custom BookStack
* Custom 'Activity' => BookStack\Facades\Activity::class,
*/ 'Setting' => BookStack\Facades\Setting::class,
'Views' => BookStack\Facades\Views::class,
'Activity' => BookStack\Services\Facades\Activity::class, 'Images' => BookStack\Facades\Images::class,
'Setting' => BookStack\Services\Facades\Setting::class,
'Views' => BookStack\Services\Facades\Views::class,
'Images' => \BookStack\Services\Facades\Images::class,
], ],
// Proxy configuration
'proxies' => env('APP_PROXIES', ''), 'proxies' => env('APP_PROXIES', ''),
]; ];

View File

@ -1,43 +1,32 @@
<?php <?php
/**
* Authentication configuration options.
*
* Changes to these config files are not supported by BookStack and may break upon updates.
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
return [ return [
// Method of authentication to use
// Options: standard, ldap
'method' => env('AUTH_METHOD', 'standard'), 'method' => env('AUTH_METHOD', 'standard'),
/* // Authentication Defaults
|-------------------------------------------------------------------------- // This option controls the default authentication "guard" and password
| Authentication Defaults // reset options for your application.
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [ 'defaults' => [
'guard' => 'web', 'guard' => 'web',
'passwords' => 'users', 'passwords' => 'users',
], ],
/* // Authentication Guards
|-------------------------------------------------------------------------- // All authentication drivers have a user provider. This defines how the
| Authentication Guards // users are actually retrieved out of your database or other storage
|-------------------------------------------------------------------------- // mechanisms used by this application to persist your user's data.
| // Supported: "session", "token"
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [ 'guards' => [
'web' => [ 'web' => [
'driver' => 'session', 'driver' => 'session',
@ -50,27 +39,15 @@ return [
], ],
], ],
/* // User Providers
|-------------------------------------------------------------------------- // All authentication drivers have a user provider. This defines how the
| User Providers // users are actually retrieved out of your database or other storage
|-------------------------------------------------------------------------- // mechanisms used by this application to persist your user's data.
| // Supported: database, eloquent, ldap
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [ 'providers' => [
'users' => [ 'users' => [
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'), 'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
'model' => BookStack\User::class, 'model' => \BookStack\Auth\User::class,
], ],
// 'users' => [ // 'users' => [
@ -79,25 +56,10 @@ return [
// ], // ],
], ],
/* // Resetting Passwords
|-------------------------------------------------------------------------- // The expire time is the number of minutes that the reset token should be
| Resetting Passwords // considered valid. This security feature keeps tokens short-lived so
|-------------------------------------------------------------------------- // they have less time to be guessed. You may change this as needed.
|
| Here you may set the options for resetting passwords including the view
| that is your password reset e-mail. You may also set the name of the
| table that maintains all of the reset tokens for your application.
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'passwords' => [ 'passwords' => [
'users' => [ 'users' => [
'provider' => 'users', 'provider' => 'users',

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