1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2024-10-30 15:42:41 +01:00

Merge pull request #1 from BookStackApp/master

merge changes
This commit is contained in:
Mantikor 2019-05-06 21:33:09 +03:00 committed by GitHub
commit 07b889547d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
372 changed files with 11341 additions and 9911 deletions

View File

@ -1,11 +1,14 @@
# Environment
APP_ENV=production
APP_DEBUG=false
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
APP_KEY=SomeRandomString
# The below url has to be set if using social auth options
# or if you are not using BookStack at the root path of your domain.
# APP_URL=http://bookstack.dev
# Application URL
# Remove the hash below and set a URL if using BookStack behind
# a proxy, if using a third-party authentication option.
# This must be the root URL that you want to host BookStack on.
# All URL's in BookStack will be generated using this value.
#APP_URL=https://example.com
# Database details
DB_HOST=localhost
@ -13,77 +16,16 @@ DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Cache and session
CACHE_DRIVER=file
SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# A different prefix is useful when multiple BookStack instances use the same caching server
CACHE_PREFIX=bookstack
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Storage
STORAGE_TYPE=local
# Amazon S3 Config
STORAGE_S3_KEY=false
STORAGE_S3_SECRET=false
STORAGE_S3_REGION=false
STORAGE_S3_BUCKET=false
# Storage URL
# Used to prefix image urls for when using custom domains/cdns
STORAGE_URL=false
# General auth
AUTH_METHOD=standard
# Social Authentication information. Defaults as off.
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# External services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Do you want to sync LDAP groups to BookStack roles for a user
LDAP_USER_TO_GROUPS=false
# What is the LDAP attribute for group memberships
LDAP_GROUP_ATTRIBUTE="memberOf"
# Would you like to remove users from roles on BookStack if they do not match on LDAP
# If false, the ldap groups-roles sync will only add users to roles
LDAP_REMOVE_FROM_GROUPS=false
# Mail settings
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
MAIL_DRIVER=smtp
# SMTP mail options
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM=null
MAIL_FROM_NAME=null
# A full list of options can be found in the '.env.example.complete' file.

227
.env.example.complete Normal file
View File

@ -0,0 +1,227 @@
# Full list of environment variables that can be used with BookStack.
# Selectively copy these to your '.env' file as required.
# Each option is shown with it's default value.
# Do not copy this whole file to use as your '.env' file.
# Application environment
# Can be 'production', 'development', 'testing' or 'demo'
APP_ENV=production
# Enable debug mode
# Shows advanced debug information and errors.
# CAN EXPOSE OTHER VARIABLES, LEAVE DISABLED
APP_DEBUG=false
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
APP_KEY=SomeRandomString
# Application URL
# This must be the root URL that you want to host BookStack on.
# All URL's in BookStack will be generated using this value.
APP_URL=https://example.com
# Application default language
# The default language choice to show.
# May be overridden by user-preference or visitor browser settings.
APP_LANG=en
# Auto-detect language for public visitors.
# Uses browser-sent headers to infer a language.
# APP_LANG will be used if such a header is not provided.
APP_AUTO_LANG_PUBLIC=true
# Application timezone
# Used where dates are displayed such as on exported content.
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
APP_TIMEZONE=UTC
# Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sending options
MAIL_FROM=mail@bookstackapp.com
MAIL_FROM_NAME=BookStack
# SMTP mail options
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# Cache & Session driver to use
# Can be 'file', 'database', 'memcached' or 'redis'
CACHE_DRIVER=file
SESSION_DRIVER=file
# Session configuration
SESSION_LIFETIME=120
SESSION_COOKIE_NAME=bookstack_session
SESSION_SECURE_COOKIE=false
# Cache key prefix
# Can be used to prevent conflicts multiple BookStack instances use the same store.
CACHE_PREFIX=bookstack
# Memcached server configuration
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Redis server configuration
# This follows the following format: HOST:PORT:DATABASE
# or, if using a password: HOST:PORT:DATABASE:PASSWORD
# For multiple servers separate with a comma. These will be clustered.
REDIS_SERVERS=127.0.0.1:6379:0
# Queue driver to use
# Queue not really currently used but may be configurable in the future.
# Would advise not to change this for now.
QUEUE_DRIVER=sync
# Storage system to use
# Can be 'local', 'local_secure' or 's3'
STORAGE_TYPE=local
# Amazon S3 storage configuration
STORAGE_S3_KEY=your-s3-key
STORAGE_S3_SECRET=your-s3-secret
STORAGE_S3_BUCKET=s3-bucket-name
STORAGE_S3_REGION=s3-bucket-region
# S3 endpoint to use for storage calls
# Only set this if using a non-Amazon s3-compatible service such as Minio
STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
# Storage URL prefix
# Used as a base for any generated image urls.
# An s3-format URL will be generated if not set.
STORAGE_URL=false
# Authentication method to use
# Can be 'standard' or 'ldap'
AUTH_METHOD=standard
# Social authentication configuration
# All disabled by default.
# Refer to https://www.bookstackapp.com/docs/admin/third-party-auth/
AZURE_APP_ID=false
AZURE_APP_SECRET=false
AZURE_TENANT=false
AZURE_AUTO_REGISTER=false
AZURE_AUTO_CONFIRM_EMAIL=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
DISCORD_AUTO_REGISTER=false
DISCORD_AUTO_CONFIRM_EMAIL=false
FACEBOOK_APP_ID=false
FACEBOOK_APP_SECRET=false
FACEBOOK_AUTO_REGISTER=false
FACEBOOK_AUTO_CONFIRM_EMAIL=false
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GITHUB_AUTO_REGISTER=false
GITHUB_AUTO_CONFIRM_EMAIL=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
GITLAB_AUTO_REGISTER=false
GITLAB_AUTO_CONFIRM_EMAIL=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
GOOGLE_AUTO_REGISTER=false
GOOGLE_AUTO_CONFIRM_EMAIL=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
OKTA_AUTO_REGISTER=false
OKTA_AUTO_CONFIRM_EMAIL=false
SLACK_APP_ID=false
SLACK_APP_SECRET=false
SLACK_AUTO_REGISTER=false
SLACK_AUTO_CONFIRM_EMAIL=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
TWITCH_AUTO_REGISTER=false
TWITCH_AUTO_CONFIRM_EMAIL=false
TWITTER_APP_ID=false
TWITTER_APP_SECRET=false
TWITTER_AUTO_REGISTER=false
TWITTER_AUTO_CONFIRM_EMAIL=false
# LDAP authentication configuration
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
LDAP_TLS_INSECURE=false
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_FOLLOW_REFERRALS=true
# LDAP group sync configuration
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
LDAP_USER_TO_GROUPS=false
LDAP_GROUP_ATTRIBUTE="memberOf"
LDAP_REMOVE_FROM_GROUPS=false
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
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.
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL=
# Enable Draw.io integration
DRAWIO=true
# Default item listing view
# Used for public visitors and user's without a preference
# Can be 'list' or 'grid'
APP_VIEWS_BOOKS=list
APP_VIEWS_BOOKSHELVES=grid
# Page revision limit
# Number of page revisions to keep in the system before deleting old revisions.
# If set to 'false' a limit will not be enforced.
REVISION_LIMIT=50
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false
# Indicate if robots/crawlers should crawl your instance.
# Can be 'true', 'false' or 'null'.
# The behaviour of the default 'null' option will depend on the 'app-public' admin setting.
# Contents of the robots.txt file can be overridden, making this option obsolete.
ALLOW_ROBOTS=null

View File

@ -103,18 +103,22 @@ class ActivityService
* @param int $page
* @return array
*/
public function entityActivity($entity, $count = 20, $page = 0)
public function entityActivity($entity, $count = 20, $page = 1)
{
if ($entity->isA('book')) {
$query = $this->activity->where('book_id', '=', $entity->id);
} else {
$query = $this->activity->where('entity_type', '=', get_class($entity))
$query = $this->activity->where('entity_type', '=', $entity->getMorphClass())
->where('entity_id', '=', $entity->id);
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
->orderBy('created_at', 'desc')
->with(['entity', 'user.avatar'])
->skip($count * ($page - 1))
->take($count)
->get();
return $this->filterSimilar($activity);
}

View File

@ -2,21 +2,26 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
use Illuminate\Support\Collection;
class ViewService
{
protected $view;
protected $permissionService;
protected $entityProvider;
/**
* ViewService constructor.
* @param \BookStack\Actions\View $view
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService)
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
{
$this->view = $view;
$this->permissionService = $permissionService;
$this->entityProvider = $entityProvider;
}
/**
@ -50,23 +55,21 @@ class ViewService
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param Entity|false|array $filterModel
* @param string|array $filterModels
* @param string $action - used for permission checking
* @return
* @return Collection
*/
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
{
// TODO - Standardise input filter
$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'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModel && is_array($filterModel)) {
$query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) {
$query->where('viewable_type', '=', $filterModel->getMorphClass());
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');

View File

@ -92,4 +92,27 @@ class Ldap
{
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

@ -80,24 +80,44 @@ class LdapService
public function getUserDetails($userName)
{
$emailAttr = $this->config['email_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
$displayNameAttr = $this->config['display_name_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
if ($user === null) {
return null;
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
];
}
/**
* Get a property from an LDAP user response fetch.
* Handles properties potentially being part of an array.
* @param array $userDetails
* @param string $propertyKey
* @param $defaultValue
* @return mixed
*/
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
{
if (isset($userDetails[$propertyKey])) {
return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
}
return $defaultValue;
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
@ -107,6 +127,7 @@ class LdapService
if ($ldapUser === null) {
return false;
}
if ($ldapUser['uid'] !== $user->external_auth_id) {
return false;
}
@ -161,15 +182,14 @@ class LdapService
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, '');
// 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);
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
$serverDetails = $this->parseServerString($this->config['server']);
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
@ -184,6 +204,27 @@ class LdapService
return $this->ldapConnection;
}
/**
* Parse a LDAP server string and return the host and port for
* a connection. Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'
* @param $serverString
* @return array
*/
protected function parseServerString($serverString)
{
$serverNameParts = explode(':', $serverString);
// If we have a protocol just return the full string since PHP will ignore a separate port.
if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
return ['host' => $serverString, 'port' => 389];
}
// Otherwise, extract the port out
$hostName = $serverNameParts[0];
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
return ['host' => $hostName, 'port' => $ldapPort];
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
@ -195,7 +236,7 @@ class LdapService
$newAttrs = [];
foreach ($attrs as $key => $attrText) {
$newKey = '${' . $key . '}';
$newAttrs[$newKey] = $attrText;
$newAttrs[$newKey] = $this->ldap->escape($attrText);
}
return strtr($filterString, $newAttrs);
}
@ -265,7 +306,8 @@ class LdapService
$baseDn = $this->config['base_dn'];
$groupsAttr = strtolower($this->config['group_attribute']);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
$groupFilter = 'CN=' . $this->ldap->escape($groupName);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]);
if ($groups['count'] === 0) {
return [];
}
@ -277,23 +319,26 @@ class LdapService
/**
* Filter out LDAP CN and DN language in a ldap search return
* Gets the base CN (common name) of the string
* @param string $ldapSearchReturn
* @param array $userGroupSearchResponse
* @return array
*/
protected function groupFilter($ldapSearchReturn)
protected function groupFilter(array $userGroupSearchResponse)
{
$groupsAttr = strtolower($this->config['group_attribute']);
$ldapGroups = [];
$count = 0;
if (isset($ldapSearchReturn[$groupsAttr]['count'])) {
$count = (int) $ldapSearchReturn[$groupsAttr]['count'];
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
$count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
}
for ($i=0; $i<$count; $i++) {
$dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i], 1);
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;
}

View File

@ -190,10 +190,10 @@ class PermissionService
{
return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
/**
@ -556,6 +556,39 @@ class PermissionService
return $q;
}
/**
* Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type.
* @param string $permission
* @param string $entityClass
* @return bool
*/
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
{
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id;
$permissionQuery = $this->db->table('joint_permissions')
->where('action', '=', $permission)
->whereIn('role_id', $userRoleIds)
->where(function ($query) use ($userId) {
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId);
});
});
if (!is_null($entityClass)) {
$entityInstance = app()->make($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
$hasPermission = $permissionQuery->count() > 0;
$this->clean();
return $hasPermission;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
@ -612,13 +645,13 @@ class PermissionService
$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);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
@ -671,7 +704,7 @@ class PermissionService
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return mixed
* @return QueryBuilder
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
@ -699,18 +732,21 @@ class PermissionService
}
/**
* Filters pages that are a direct relation to another item.
* Add conditions to a query to filter the selection to related entities
* where permissions are granted.
* @param $entityType
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
{
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$pageMorphClass = $this->entityProvider->page->getMorphClass();
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
@ -728,7 +764,9 @@ class PermissionService
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
$this->clean();
return $q;
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Auth;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Model;
class Role extends Model
@ -13,7 +14,7 @@ class Role extends Model
*/
public function users()
{
return $this->belongsToMany(User::class);
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
@ -30,7 +31,7 @@ class Role extends Model
*/
public function permissions()
{
return $this->belongsToMany(Permissions\RolePermission::class, 'permission_role', 'role_id', 'permission_id');
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}
/**
@ -51,18 +52,18 @@ class Role extends Model
/**
* Add a permission to this role.
* @param \BookStack\Auth\Permissions\RolePermission $permission
* @param RolePermission $permission
*/
public function attachPermission(Permissions\RolePermission $permission)
public function attachPermission(RolePermission $permission)
{
$this->permissions()->attach($permission->id);
}
/**
* Detach a single permission from this role.
* @param \BookStack\Auth\Permissions\RolePermission $permission
* @param RolePermission $permission
*/
public function detachPermission(Permissions\RolePermission $permission)
public function detachPermission(RolePermission $permission)
{
$this->permissions()->detach($permission->id);
}

View File

@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
* The attributes that are mass assignable.
* @var array
*/
protected $fillable = ['name', 'email', 'image_id'];
protected $fillable = ['name', 'email'];
/**
* The attributes excluded from the model's JSON form.

View File

@ -3,8 +3,10 @@
use Activity;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Images;
class UserRepo
@ -42,12 +44,12 @@ class UserRepo
*/
public function getById($id)
{
return $this->user->findOrFail($id);
return $this->user->newQuery()->findOrFail($id);
}
/**
* Get all the users with their permissions.
* @return \Illuminate\Database\Eloquent\Builder|static
* @return Builder|static
*/
public function getAllUsers()
{
@ -58,7 +60,7 @@ class UserRepo
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
* @return Builder|static
*/
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
@ -85,9 +87,7 @@ class UserRepo
{
$user = $this->create($data, $verifyEmail);
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
$this->downloadGravatarToUserAvatar($user);
$this->downloadAndAssignUserAvatar($user);
return $user;
}
@ -137,6 +137,40 @@ class UserRepo
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.
* @param array $data
@ -145,7 +179,6 @@ class UserRepo
*/
public function create(array $data, $verifyEmail = false)
{
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
@ -165,7 +198,7 @@ class UserRepo
$user->delete();
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
@ -191,16 +224,15 @@ class UserRepo
*/
public function getRecentlyCreated(User $user, $count = 20)
{
$createdByUserQuery = function (Builder $query) use ($user) {
$query->where('created_by', '=', $user->id);
};
return [
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
];
}
@ -215,6 +247,7 @@ class UserRepo
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
];
}
@ -224,7 +257,7 @@ class UserRepo
*/
public function getAllRoles()
{
return $this->role->all();
return $this->role->newQuery()->orderBy('name', 'asc')->get();
}
/**
@ -238,25 +271,24 @@ class UserRepo
}
/**
* Get a gravatar image for a user and set it as their avatar.
* Does not run if gravatar disabled in config.
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
* @param User $user
* @return bool
*/
public function downloadGravatarToUserAvatar(User $user)
public function downloadAndAssignUserAvatar(User $user)
{
// Get avatar from gravatar and save
if (!config('services.gravatar')) {
if (!Images::avatarFetchEnabled()) {
return false;
}
try {
$avatar = Images::saveUserGravatar($user);
$avatar = Images::saveUserAvatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
\Log::error('Failed to save user avatar image');
return false;
}
}

View File

@ -76,7 +76,7 @@ class CreateAdmin extends Command
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
$this->userRepo->attachSystemRole($user, 'admin');
$this->userRepo->downloadGravatarToUserAvatar($user);
$this->userRepo->downloadAndAssignUserAvatar($user);
$user->email_confirmed = true;
$user->save();

View File

@ -38,7 +38,7 @@ class Book extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
return $default;
}
@ -69,6 +69,15 @@ class Book extends Entity
return $this->hasMany(Page::class);
}
/**
* Get the direct child pages of this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function directPages()
{
return $this->pages()->where('chapter_id', '=', '0');
}
/**
* Get all chapters within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
@ -92,7 +101,7 @@ class Book extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;

View File

@ -26,7 +26,9 @@ class Bookshelf extends Entity
*/
public function books()
{
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')
->withPivot('order')
->orderBy('order', 'asc');
}
/**
@ -50,7 +52,8 @@ class Bookshelf extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
// TODO - Make generic, focused on books right now, Perhaps set-up a better image
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
return $default;
}
@ -64,7 +67,7 @@ class Bookshelf extends Entity
}
/**
* Get the cover image of the book
* Get the cover image of the shelf
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
@ -77,7 +80,7 @@ class Bookshelf extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
@ -91,4 +94,14 @@ class Bookshelf extends Entity
{
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";
}
/**
* Check if this shelf contains the given book.
* @param Book $book
* @return bool
*/
public function contains(Book $book)
{
return $this->books()->where('id', '=', $book->id)->count() > 0;
}
}

View File

@ -0,0 +1,34 @@
<?php namespace BookStack\Entities;
use Illuminate\View\View;
class BreadcrumbsViewComposer
{
protected $entityContextManager;
/**
* BreadcrumbsViewComposer constructor.
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityContextManager $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
/**
* Modify data when the view is composed.
* @param View $view
*/
public function compose(View $view)
{
$crumbs = $view->getData()['crumbs'];
if (array_first($crumbs) instanceof Book) {
$shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
if ($shelf) {
array_unshift($crumbs, $shelf);
$view->with('crumbs', $crumbs);
}
}
}
}

View File

@ -53,9 +53,9 @@ class Chapter extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
$description = $this->text ?? $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
@ -67,4 +67,13 @@ class Chapter extends Entity
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
/**
* Check if this chapter has any child pages.
* @return bool
*/
public function hasChildren()
{
return count($this->pages) > 0;
}
}

View File

@ -102,6 +102,11 @@ class Entity extends Ownable
return $this->morphMany(View::class, 'viewable');
}
public function viewCountQuery()
{
return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
}
/**
* Get the Tag models that have been user assigned to this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@ -218,6 +223,20 @@ class Entity extends Ownable
return $this->{$this->textField};
}
/**
* Get an excerpt of this entity's descriptive content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt(int $length = 100)
{
$text = $this->getText();
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length-3) . '...';
}
return trim($text);
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string

View File

@ -0,0 +1,60 @@
<?php namespace BookStack\Entities;
use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Session\Store;
class EntityContextManager
{
protected $session;
protected $entityRepo;
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
/**
* EntityContextManager constructor.
* @param Store $session
* @param EntityRepo $entityRepo
*/
public function __construct(Store $session, EntityRepo $entityRepo)
{
$this->session = $session;
$this->entityRepo = $entityRepo;
}
/**
* Get the current bookshelf context for the given book.
* @param Book $book
* @return Bookshelf|null
*/
public function getContextualShelfForBook(Book $book)
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
if (is_int($contextBookshelfId)) {
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
if ($shelf && $shelf->contains($book)) {
return $shelf;
}
}
return null;
}
/**
* Store the current contextual shelf ID.
* @param int $shelfId
*/
public function setShelfContext(int $shelfId)
{
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
}
/**
* Clear the session stored shelf context id.
*/
public function clearShelfContext()
{
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
}
}

View File

@ -85,5 +85,22 @@ class EntityProvider
return $this->all()[$type];
}
/**
* Get the morph classes, as an array, for a single or multiple types.
* @param string|array $types
* @return array<string>
*/
public function getMorphClasses($types)
{
if (is_string($types)) {
$types = [$types];
}
}
$morphClasses = [];
foreach ($types as $type) {
$model = $this->get($type);
$morphClasses[] = $model->getMorphClass();
}
return $morphClasses;
}
}

View File

@ -102,17 +102,6 @@ class Page extends Entity
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
}
/**
* Get an excerpt of this page's content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt($length = 100)
{
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
return mb_convert_encoding($text, 'UTF-8');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent

View File

@ -1,5 +1,6 @@
<?php namespace BookStack\Entities\Repos;
use Activity;
use BookStack\Actions\TagRepo;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
@ -15,8 +16,13 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Uploads\AttachmentService;
use DOMDocument;
use DOMNode;
use DOMXPath;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Throwable;
class EntityRepo
{
@ -101,7 +107,7 @@ class EntityRepo
* @param integer $id
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
{
@ -119,7 +125,7 @@ class EntityRepo
* @param []int $ids
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
* @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
*/
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
{
@ -137,7 +143,7 @@ class EntityRepo
* @param string $type
* @param string $slug
* @param string|bool $bookSlug
* @return \BookStack\Entities\Entity
* @return Entity
* @throws NotFoundException
*/
public function getBySlug($type, $slug, $bookSlug = false)
@ -179,11 +185,38 @@ class EntityRepo
* Get all entities in a paginated format
* @param $type
* @param int $count
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
* @param string $sort
* @param string $order
* @param null|callable $queryAddition
* @return LengthAwarePaginator
*/
public function getAllPaginated($type, $count = 10)
public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
{
return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
$query = $this->entityQuery($type);
$query = $this->addSortToQuery($query, $sort, $order);
if ($queryAddition) {
$queryAddition($query);
}
return $query->paginate($count);
}
/**
* Add sorting operations to an entity query.
* @param Builder $query
* @param string $sort
* @param string $order
* @return Builder
*/
protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
{
$order = ($order === 'asc') ? 'asc' : 'desc';
$propertySorts = ['name', 'created_at', 'updated_at'];
if (in_array($sort, $propertySorts)) {
return $query->orderBy($sort, $order);
}
return $query;
}
/**
@ -265,15 +298,14 @@ class EntityRepo
/**
* Get the most popular entities base on all views.
* @param string|bool $type
* @param string $type
* @param int $count
* @param int $page
* @return mixed
*/
public function getPopular($type, $count = 10, $page = 0)
public function getPopular(string $type, int $count = 10, int $page = 0)
{
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
return $this->viewService->getPopular($count, $page, $filter);
return $this->viewService->getPopular($count, $page, $type);
}
/**
@ -305,7 +337,7 @@ class EntityRepo
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param \BookStack\Entities\Bookshelf $bookshelf
* @param Bookshelf $bookshelf
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getBookshelfChildren(Bookshelf $bookshelf)
@ -313,11 +345,23 @@ class EntityRepo
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get the direct children of a book.
* @param Book $book
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getBookDirectChildren(Book $book)
{
$pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
$chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param \BookStack\Entities\Book $book
* @param Book $book
* @param bool $filterDrafts
* @param bool $renderPages
* @return mixed
@ -367,7 +411,7 @@ class EntityRepo
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param \BookStack\Entities\Chapter $chapter
* @param Chapter $chapter
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getChapterChildren(Chapter $chapter)
@ -379,7 +423,7 @@ class EntityRepo
/**
* Get the next sequential priority for a new child element in the given book.
* @param \BookStack\Entities\Book $book
* @param Book $book
* @return int
*/
public function getNewBookPriority(Book $book)
@ -390,7 +434,7 @@ class EntityRepo
/**
* Get a new priority for a new page to be added to the given chapter.
* @param \BookStack\Entities\Chapter $chapter
* @param Chapter $chapter
* @return int
*/
public function getNewChapterPriority(Chapter $chapter)
@ -439,8 +483,8 @@ class EntityRepo
/**
* Updates entity restrictions from a request
* @param Request $request
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
* @param Entity $entity
* @throws Throwable
*/
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
{
@ -470,7 +514,7 @@ class EntityRepo
* @param string $type
* @param array $input
* @param bool|Book $book
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function createFromInput($type, $input = [], $book = false)
{
@ -494,9 +538,9 @@ class EntityRepo
* Update entity details from request input.
* Used for books and chapters
* @param string $type
* @param \BookStack\Entities\Entity $entityModel
* @param Entity $entityModel
* @param array $input
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function updateFromInput($type, Entity $entityModel, $input = [])
{
@ -519,7 +563,7 @@ class EntityRepo
/**
* Sync the books assigned to a shelf from a comma-separated list
* of book IDs.
* @param \BookStack\Entities\Bookshelf $shelf
* @param Bookshelf $shelf
* @param string $books
*/
public function updateShelfBooks(Bookshelf $shelf, string $books)
@ -538,13 +582,28 @@ class EntityRepo
$shelf->books()->sync($syncData);
}
/**
* Append a Book to a BookShelf.
* @param Bookshelf $shelf
* @param Book $book
*/
public function appendBookToShelf(Bookshelf $shelf, Book $book)
{
if ($shelf->contains($book)) {
return;
}
$maxOrder = $shelf->books()->max('order');
$shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
}
/**
* Change the book that an entity belongs to.
* @param string $type
* @param integer $newBookId
* @param Entity $entity
* @param bool $rebuildPermissions
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
{
@ -599,24 +658,48 @@ class EntityRepo
}
/**
* Render the page for viewing, Parsing and performing features such as page transclusion.
* Render the page for viewing
* @param Page $page
* @param bool $ignorePermissions
* @return mixed|string
* @param bool $blankIncludes
* @return string
*/
public function renderPage(Page $page, $ignorePermissions = false)
public function renderPage(Page $page, bool $blankIncludes = false) : string
{
$content = $page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
}
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) {
return $content;
if ($blankIncludes) {
$content = $this->blankPageIncludes($content);
} else {
$content = $this->parsePageIncludes($content);
}
return $content;
}
/**
* Remove any page include tags within the given HTML.
* @param string $html
* @return string
*/
protected function blankPageIncludes(string $html) : string
{
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
}
/**
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
* @param string $html
* @return mixed|string
*/
protected function parsePageIncludes(string $html) : string
{
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
$splitInclude = explode('#', $includeId, 2);
@ -625,22 +708,23 @@ class EntityRepo
continue;
}
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
$matchedPage = $this->getById('page', $pageId);
if ($matchedPage === null) {
$content = str_replace($matches[0][$index], '', $content);
$html = str_replace($matches[0][$index], '', $html);
continue;
}
if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
$html = str_replace($matches[0][$index], $matchedPage->html, $html);
continue;
}
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content);
$html = str_replace($matches[0][$index], '', $html);
continue;
}
$innerContent = '';
@ -652,29 +736,49 @@ class EntityRepo
$innerContent .= $doc->saveHTML($childNode);
}
}
$content = str_replace($matches[0][$index], trim($innerContent), $content);
libxml_clear_errors();
$html = str_replace($matches[0][$index], trim($innerContent), $html);
}
return $content;
return $html;
}
/**
* Escape script tags within HTML content.
* @param string $html
* @return mixed
* @return string
*/
protected function escapeScripts(string $html)
protected function escapeScripts(string $html) : string
{
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) {
if ($html == '') {
return $html;
}
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//body//*//script');
foreach ($scriptElems as $scriptElem) {
$scriptElem->parentNode->removeChild($scriptElem);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//body//*/@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$html .= $doc->saveHTML($child);
}
return $html;
}
@ -685,7 +789,7 @@ class EntityRepo
*/
public function searchForImage($imageString)
{
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
@ -696,8 +800,8 @@ class EntityRepo
/**
* Destroy a bookshelf instance
* @param \BookStack\Entities\Bookshelf $shelf
* @throws \Throwable
* @param Bookshelf $shelf
* @throws Throwable
*/
public function destroyBookshelf(Bookshelf $shelf)
{
@ -707,9 +811,9 @@ class EntityRepo
/**
* Destroy the provided book and all its child entities.
* @param \BookStack\Entities\Book $book
* @param Book $book
* @throws NotifyException
* @throws \Throwable
* @throws Throwable
*/
public function destroyBook(Book $book)
{
@ -725,8 +829,8 @@ class EntityRepo
/**
* Destroy a chapter and its relations.
* @param \BookStack\Entities\Chapter $chapter
* @throws \Throwable
* @param Chapter $chapter
* @throws Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@ -744,7 +848,7 @@ class EntityRepo
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
* @throws \Throwable
* @throws Throwable
*/
public function destroyPage(Page $page)
{
@ -767,12 +871,12 @@ class EntityRepo
/**
* Destroy or handle the common relations connected to an entity.
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
* @param Entity $entity
* @throws Throwable
*/
protected function destroyEntityCommonRelations(Entity $entity)
{
\Activity::removeEntity($entity);
Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
@ -784,9 +888,9 @@ class EntityRepo
/**
* Copy the permissions of a bookshelf to all child books.
* Returns the number of books that had permissions updated.
* @param \BookStack\Entities\Bookshelf $bookshelf
* @param Bookshelf $bookshelf
* @return int
* @throws \Throwable
* @throws Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{

View File

@ -7,6 +7,7 @@ use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use Carbon\Carbon;
use DOMDocument;
use DOMElement;
use DOMXPath;
class PageRepo extends EntityRepo
@ -129,8 +130,7 @@ class PageRepo extends EntityRepo
}
/**
* Formats a page's html to be tagged correctly
* within the system.
* Formats a page's html to be tagged correctly within the system.
* @param string $htmlText
* @return string
*/
@ -139,6 +139,7 @@ class PageRepo extends EntityRepo
if ($htmlText == '') {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
@ -147,37 +148,17 @@ class PageRepo extends EntityRepo
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$idArray = [];
// Set ids on top-level nodes
$idMap = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') {
continue;
}
$this->setUniqueId($childNode, $idMap);
}
// 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;
// Ensure no duplicate ids within child items
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
$this->setUniqueId($domElem, $idMap);
}
// Generate inner html as a string
@ -189,14 +170,49 @@ class PageRepo extends EntityRepo
return $html;
}
/**
* Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence.
* @param DOMElement $element
* @param array $idMap
*/
protected function setUniqueId($element, array &$idMap)
{
if (get_class($element) !== 'DOMElement') {
return;
}
// Overwrite id if not a BookStack custom id
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return;
}
// 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($element->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (isset($idMap[$newId])) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$element->setAttribute('id', $newId);
$idMap[$newId] = true;
}
/**
* Get the plain text version of a page's content.
* @param \BookStack\Entities\Page $page
* @return string
*/
public function pageToPlainText(Page $page)
protected function pageToPlainText(Page $page) : string
{
$html = $this->renderPage($page);
$html = $this->renderPage($page, true);
return strip_tags($html);
}
@ -505,4 +521,4 @@ class PageRepo extends EntityRepo
return $this->publishPageDraft($copyPage, $pageData);
}
}
}

View File

@ -0,0 +1,7 @@
<?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 $redirectLocation
*/
public function __construct($message, $redirectLocation)
public function __construct(string $message, string $redirectLocation = "/")
{
$this->message = $message;
$this->redirectLocation = $redirectLocation;

View File

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

View File

@ -128,7 +128,7 @@ class LoginController extends Controller
]);
}
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**

View File

@ -70,7 +70,7 @@ class RegisterController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'name' => 'required|min:2|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6',
]);
@ -176,7 +176,7 @@ class RegisterController extends Controller
*/
public function getRegisterConfirmation()
{
return view('auth/register-confirm');
return view('auth.register-confirm');
}
/**
@ -204,7 +204,7 @@ class RegisterController extends Controller
*/
public function showAwaitingConfirmation()
{
return view('auth/user-unconfirmed');
return view('auth.user-unconfirmed');
}
/**

View File

@ -3,8 +3,10 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Book;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@ -15,18 +17,29 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param \BookStack\Auth\UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param UserRepo $userRepo
* @param ExportService $exportService
* @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
public function __construct(
EntityRepo $entityRepo,
UserRepo $userRepo,
ExportService $exportService,
EntityContextManager $entityContextManager,
ImageRepo $imageRepo
) {
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@ -36,67 +49,117 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 18);
$view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
$sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
$order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.books'));
return view('books/index', [
return view('books.index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'booksViewType' => $booksViewType
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
/**
* Show the form for creating a new book.
* @param string $shelfSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function create()
public function create(string $shelfSlug = null)
{
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$this->checkPermission('book-create-all');
$this->setPageTitle(trans('entities.books_create'));
return view('books/create');
return view('books.create', [
'bookshelf' => $bookshelf
]);
}
/**
* Store a newly created book in storage.
*
* @param Request $request
* @param Request $request
* @param string $shelfSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request)
public function store(Request $request, string $shelfSlug = null)
{
$this->checkPermission('book-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$book = $this->entityRepo->createFromInput('book', $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
$this->entityRepo->appendBookToShelf($bookshelf, $book);
Activity::add($bookshelf, 'bookshelf_update');
}
return redirect($book->getUrl());
}
/**
* Display the specified book.
* @param $slug
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show($slug)
public function show($slug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}
$this->setPageTitle($book->getShortName());
return view('books/show', [
return view('books.show', [
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'activity' => Activity::entityActivity($book, 20, 0)
'activity' => Activity::entityActivity($book, 20, 1)
]);
}
@ -110,25 +173,32 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
return view('books.edit', ['book' => $book, 'current' => $book]);
}
/**
* Update the specified book in storage.
* @param Request $request
* @param Request $request
* @param $slug
* @return Response
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, $slug)
public function update(Request $request, string $slug)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@ -142,22 +212,24 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
return view('books/delete', ['book' => $book, 'current' => $book]);
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
* Shows the view which allows pages to be re-ordered and sorted.
* @param string $bookSlug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function sort($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
@ -170,7 +242,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$bookChildren = $this->entityRepo->getBookChildren($book);
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
@ -254,7 +326,12 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
if ($book->cover) {
$this->imageRepo->destroyImage($book->cover);
}
$this->entityRepo->destroyBook($book);
return redirect('/books');
}
@ -263,12 +340,12 @@ class BookController extends Controller
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
public function showPermissions($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
return view('books.permissions', [
'book' => $book,
'roles' => $roles
]);
@ -277,11 +354,12 @@ class BookController extends Controller
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, Request $request)
public function permissions($bookSlug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
@ -325,4 +403,29 @@ class BookController extends Controller
$textContent = $this->exportService->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
/**
* Common actions to run on book update.
* Handles updating the cover image.
* @param Book $book
* @param Request $request
* @throws \BookStack\Exceptions\ImageUploadException
*/
protected function bookUpdateActions(Book $book, Request $request)
{
// Update the cover image if in request
if ($request->has('image')) {
$this->imageRepo->destroyImage($book->cover);
$newImage = $request->file('image');
$image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
$book->image_id = $image->id;
$book->save();
}
if ($request->has('image_reset')) {
$this->imageRepo->destroyImage($book->cover);
$book->image_id = 0;
$book->save();
}
}
}

View File

@ -3,8 +3,9 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@ -14,19 +15,22 @@ class BookshelfController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@ -36,19 +40,35 @@ class BookshelfController extends Controller
*/
public function index()
{
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
$order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
foreach ($shelves as $shelf) {
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
}
$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->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
return view('shelves/index', [
return view('shelves.index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'shelvesViewType' => $shelvesViewType
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
@ -61,13 +81,14 @@ class BookshelfController extends Controller
$this->checkPermission('bookshelf-create-all');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
return view('shelves.create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
* @param Request $request
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request)
{
@ -75,13 +96,14 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
Activity::add($bookshelf, 'bookshelf_create');
$shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->shelfUpdateActions($shelf, $request);
return redirect($bookshelf->getUrl());
Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
}
@ -93,17 +115,20 @@ class BookshelfController extends Controller
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('book-view', $shelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$books = $this->entityRepo->getBookshelfChildren($shelf);
Views::add($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
$this->setPageTitle($shelf->getShortName());
return view('shelves.show', [
'shelf' => $shelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
'activity' => Activity::entityActivity($shelf, 20, 1)
]);
}
@ -115,19 +140,19 @@ class BookshelfController extends Controller
*/
public function edit(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
$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,
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
return view('shelves.edit', [
'shelf' => $shelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
@ -136,10 +161,11 @@ class BookshelfController extends Controller
/**
* Update the specified bookshelf in storage.
* @param Request $request
* @param Request $request
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function update(Request $request, string $slug)
{
@ -148,10 +174,12 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
$this->shelfUpdateActions($shelf, $request);
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
@ -166,11 +194,11 @@ class BookshelfController extends Controller
*/
public function showDelete(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
return view('shelves.delete', ['shelf' => $shelf]);
}
/**
@ -182,46 +210,52 @@ class BookshelfController extends Controller
*/
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);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $shelf);
Activity::addMessage('bookshelf_delete', 0, $shelf->name);
if ($shelf->cover) {
$this->imageRepo->destroyImage($shelf->cover);
}
$this->entityRepo->destroyBookshelf($shelf);
return redirect('/shelves');
}
/**
* Show the Restrictions view.
* @param $slug
* Show the permissions view.
* @param string $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
public function showPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
'shelf' => $bookshelf,
return view('shelves.permissions', [
'shelf' => $shelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* Set the permissions for this bookshelf.
* @param string $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict(string $slug, Request $request)
public function permissions(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($bookshelf->getUrl());
return redirect($shelf->getUrl());
}
/**
@ -232,11 +266,38 @@ class BookshelfController extends Controller
*/
public function copyPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($bookshelf->getUrl());
return redirect($shelf->getUrl());
}
/**
* Common actions to run on bookshelf update.
* @param Bookshelf $shelf
* @param Request $request
* @throws \BookStack\Exceptions\ImageUploadException
*/
protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
{
// Update the books that the shelf references
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
// Update the cover image if in request
if ($request->has('image')) {
$newImage = $request->file('image');
$this->imageRepo->destroyImage($shelf->cover);
$image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
$shelf->image_id = $image->id;
$shelf->save();
}
if ($request->has('image_reset')) {
$this->imageRepo->destroyImage($shelf->cover);
$shelf->image_id = 0;
$shelf->save();
}
}
}

View File

@ -39,7 +39,7 @@ class ChapterController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters/create', ['book' => $book, 'current' => $book]);
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
@ -78,7 +78,7 @@ class ChapterController extends Controller
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
$pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters/show', [
return view('chapters.show', [
'book' => $chapter->book,
'chapter' => $chapter,
'current' => $chapter,
@ -98,7 +98,7 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@ -130,7 +130,7 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@ -161,7 +161,8 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
return view('chapters/move', [
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters.move', [
'chapter' => $chapter,
'book' => $chapter->book
]);
@ -179,6 +180,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@ -212,13 +214,14 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict($bookSlug, $chapterSlug)
public function showPermissions($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
return view('chapters.permissions', [
'chapter' => $chapter,
'roles' => $roles
]);
@ -230,8 +233,10 @@ class ChapterController extends Controller
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
public function permissions($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);

View File

@ -54,7 +54,7 @@ class CommentController extends Controller
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
Activity::add($page, 'commented_on', $page->book->id);
return view('comments/comment', ['comment' => $comment]);
return view('comments.comment', ['comment' => $comment]);
}
/**
@ -75,7 +75,7 @@ class CommentController extends Controller
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
return view('comments/comment', ['comment' => $comment]);
return view('comments.comment', ['comment' => $comment]);
}
/**

View File

@ -123,6 +123,20 @@ abstract class Controller extends BaseController
return true;
}
/**
* Check if the current user has a permission or bypass if the provided user
* id matches the current user.
* @param string $permissionName
* @param int $userId
* @return bool
*/
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
{
return $this->checkPermissionOr($permissionName, function () use ($userId) {
return $userId === $this->currentUser->id;
});
}
/**
* Send back a json error message.
* @param string $messageText

View File

@ -19,7 +19,6 @@ class HomeController extends Controller
parent::__construct();
}
/**
* Display the homepage.
* @return Response
@ -45,17 +44,36 @@ class HomeController extends Controller
'draftPages' => $draftPages,
];
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
$view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
$sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
$order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$commonData = array_merge($commonData, [
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
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]);
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['shelves' => $shelves]);
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]);
$books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
return view('common.home-book', $data);
}
@ -105,7 +123,7 @@ class HomeController extends Controller
*/
public function customHeadContent()
{
return view('partials/custom-head-content');
return view('partials.custom-head-content');
}
/**
@ -120,7 +138,7 @@ class HomeController extends Controller
$allowRobots = $sitePublic;
}
return response()
->view('common/robots', ['allowRobots' => $allowRobots])
->view('common.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
@ -129,6 +147,6 @@ class HomeController extends Controller
*/
public function getNotFound()
{
return response()->view('errors/404', [], 404);
return response()->view('errors.404', [], 404);
}
}

View File

@ -1,247 +0,0 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Get all images for a specific type, Paginated
* @param string $type
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
public function getAllByType($type, $page = 0)
{
$imgData = $this->imageRepo->getPaginatedByType($type, $page);
return response()->json($imgData);
}
/**
* Search through images within a particular type.
* @param $type
* @param int $page
* @param Request $request
* @return mixed
*/
public function searchByType(Request $request, $type, $page = 0)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
return response()->json($imgData);
}
/**
* Get all images for a user.
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
public function getAllForUserType($page = 0)
{
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
return response()->json($imgData);
}
/**
* Get gallery images with a specific filter such as book or page
* @param $filter
* @param int $page
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getGalleryFiltered(Request $request, $filter, $page = 0)
{
$this->validate($request, [
'page_id' => 'required|integer'
]);
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) {
return response('Invalid filter', 500);
}
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
return response()->json($imgData);
}
/**
* Handles image uploads for use on pages.
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function uploadByType($type, Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'is_image'
]);
if (!$this->imageRepo->isValidType($type)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$imageUpload = $request->file('file');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Upload a drawing to the system.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function uploadDrawing(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getBase64Image($id)
{
$image = $this->imageRepo->getById($id);
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
/**
* Generate a sized thumbnail for an image.
* @param $id
* @param $width
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function getThumbnail($id, $width, $height, $crop)
{
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]);
}
/**
* Update image details
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($imageId, Request $request)
{
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($imageId);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Show the usage of an image on pages.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class DrawioImageController extends Controller
{
protected $imageRepo;
/**
* DrawioImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$uploadedToFilter = $request->get('uploaded_to', null);
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new gallery image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getAsBase64($id)
{
$image = $this->imageRepo->getById($id);
$page = $image->getPage();
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
return $this->jsonError("Image data could not be found");
}
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class GalleryImageController extends Controller
{
protected $imageRepo;
/**
* GalleryImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$uploadedToFilter = $request->get('uploaded_to', null);
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new gallery image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => $this->imageRepo->getImageValidationRules()
]);
try {
$imageUpload = $request->file('file');
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
}

View File

@ -0,0 +1,115 @@
<?php namespace BookStack\Http\Controllers\Images;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Update image details
* @param integer $id
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($id, Request $request)
{
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Show the usage of an image on pages.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->checkImagePermission($image);
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
/**
* Check related page permission and ensure type is drawio or gallery.
* @param Image $image
*/
protected function checkImagePermission(Image $image)
{
if ($image->type !== 'drawio' && $image->type !== 'gallery') {
$this->showPermissionError();
}
$relatedPage = $image->getPage();
if ($relatedPage) {
$this->checkOwnablePermission('page-view', $relatedPage);
}
}
}

View File

@ -61,7 +61,7 @@ class PageController extends Controller
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
return view('pages.guest-create', ['parent' => $parent]);
}
/**
@ -110,7 +110,7 @@ class PageController extends Controller
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
return view('pages.edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
@ -184,7 +184,7 @@ class PageController extends Controller
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
return view('pages.show', [
'page' => $page,'book' => $page->book,
'current' => $page,
'sidebarTree' => $sidebarTree,
@ -239,7 +239,7 @@ class PageController extends Controller
}
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
@ -317,7 +317,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$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]);
}
@ -333,7 +333,7 @@ class PageController extends Controller
$page = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
$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]);
}
/**
@ -377,12 +377,13 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showRevisions($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$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, 'current' => $page]);
}
/**
@ -403,9 +404,10 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => null,
'revision' => $revision
]);
}
@ -432,7 +434,7 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => $diff,
@ -482,12 +484,12 @@ class PageController extends Controller
// 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);
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]);
return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
@ -532,49 +534,20 @@ class PageController extends Controller
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyCreated()
{
$pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [
'title' => trans('entities.recently_created_pages'),
'pages' => $pages
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyUpdated()
{
// TODO - Still exist?
$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'),
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Show the view to choose a new parent to move a page into.
* @param string $bookSlug
@ -586,7 +559,8 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
return view('pages/move', [
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
'book' => $page->book,
'page' => $page
]);
@ -604,6 +578,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@ -641,9 +616,9 @@ class PageController extends Controller
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
return view('pages.copy', [
'book' => $page->book,
'page' => $page
]);
@ -660,7 +635,7 @@ class PageController extends Controller
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@ -688,6 +663,24 @@ class PageController extends Controller
return redirect($pageCopy->getUrl());
}
/**
* Show the Permissions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws NotFoundException
*/
public function showPermissions($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages.permissions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Set the permissions for this page.
* @param string $bookSlug
@ -695,8 +688,9 @@ class PageController extends Controller
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, $pageSlug, Request $request)
public function permissions($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);

View File

@ -26,7 +26,7 @@ class PermissionController extends Controller
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
return view('settings/roles/index', ['roles' => $roles]);
return view('settings.roles.index', ['roles' => $roles]);
}
/**
@ -36,7 +36,7 @@ class PermissionController extends Controller
public function createRole()
{
$this->checkPermission('user-roles-manage');
return view('settings/roles/create');
return view('settings.roles.create');
}
/**
@ -70,7 +70,7 @@ class PermissionController extends Controller
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
return view('settings/roles/edit', ['role' => $role]);
return view('settings.roles.edit', ['role' => $role]);
}
/**
@ -106,7 +106,7 @@ class PermissionController extends Controller
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
}
/**

View File

@ -1,34 +1,45 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\SearchService;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
protected $entityContextManager;
/**
* SearchController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
public function __construct(
EntityRepo $entityRepo,
ViewService $viewService,
SearchService $searchService,
EntityContextManager $entityContextManager
) {
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
/**
* Searches all entities.
* @param Request $request
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function search(Request $request)
@ -41,7 +52,7 @@ class SearchController extends Controller
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
return view('search/all', [
return view('search.all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
@ -55,28 +66,28 @@ class SearchController extends Controller
* Searches all entities within a book.
* @param Request $request
* @param integer $bookId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchBook(Request $request, $bookId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
return view('partials/entity-list', ['entities' => $results]);
return view('partials.entity-list', ['entities' => $results]);
}
/**
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
return view('partials.entity-list', ['entities' => $results]);
}
/**
@ -87,21 +98,64 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false);
$permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
return view('search.entity-ajax-list', ['entities' => $entities]);
}
/**
* Search siblings items in the system.
* @param Request $request
* @return Factory|View|mixed
*/
public function searchSiblings(Request $request)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
$entity = $this->entityRepo->getById($type, $id);
if (!$entity) {
return $this->jsonError(trans('errors.entity_not_found'), 404);
}
$entities = [];
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
$entities = $this->entityRepo->getChapterChildren($entity->chapter);
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
$entities = $this->entityRepo->getBookDirectChildren($entity->book);
}
// Book
// Gets just the books in a shelf if shelf is in context
if ($entity->isA('book')) {
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
if ($contextShelf) {
$entities = $this->entityRepo->getBookshelfChildren($contextShelf);
} else {
$entities = $this->entityRepo->getAll('book');
}
}
// Shelve
if ($entity->isA('bookshelf')) {
$entities = $this->entityRepo->getAll('bookshelf');
}
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}
}

View File

@ -1,5 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Auth\User;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -7,6 +9,19 @@ use Setting;
class SettingController extends Controller
{
protected $imageRepo;
/**
* SettingController constructor.
* @param $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Display a listing of the settings.
* @return Response
@ -19,7 +34,10 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/index', ['version' => $version]);
return view('settings.index', [
'version' => $version,
'guestUser' => User::getDefault()
]);
}
/**
@ -31,6 +49,9 @@ class SettingController extends Controller
{
$this->preventAccessForDemoUsers();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
]);
// Cycles through posted settings and update them
foreach ($request->all() as $name => $value) {
@ -38,7 +59,21 @@ class SettingController extends Controller
continue;
}
$key = str_replace('setting-', '', trim($name));
Setting::put($key, $value);
setting()->put($key, $value);
}
// Update logo image if set
if ($request->has('app_logo')) {
$logoFile = $request->file('app_logo');
$this->imageRepo->destroyByType('system');
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
setting()->put('app-logo', $image->url);
}
// Clear logo image if requested
if ($request->get('app_logo_reset', null)) {
$this->imageRepo->destroyByType('system');
setting()->remove('app-logo');
}
session()->flash('success', trans('settings.settings_save_success'));
@ -57,7 +92,7 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/maintenance', ['version' => $version]);
return view('settings.maintenance', ['version' => $version]);
}
/**

View File

@ -3,6 +3,8 @@
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -11,16 +13,19 @@ class UserController extends Controller
protected $user;
protected $userRepo;
protected $imageRepo;
/**
* UserController constructor.
* @param User $user
* @param \BookStack\Auth\UserRepo $userRepo
* @param User $user
* @param UserRepo $userRepo
* @param ImageRepo $imageRepo
*/
public function __construct(User $user, UserRepo $userRepo)
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@ -40,7 +45,7 @@ class UserController extends Controller
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
}
/**
@ -52,13 +57,14 @@ class UserController extends Controller
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAllRoles();
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
/**
* Store a newly created user in storage.
* @param Request $request
* @return Response
* @throws UserUpdateException
*/
public function store(Request $request)
{
@ -89,10 +95,10 @@ class UserController extends Controller
if ($request->filled('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');
}
@ -105,9 +111,7 @@ class UserController extends Controller
*/
public function edit($id, SocialAuthService $socialAuthService)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->user->findOrFail($id);
@ -116,37 +120,38 @@ class UserController extends Controller
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
return view('users.edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
}
/**
* Update the specified user in storage.
* @param Request $request
* @param int $id
* @param Request $request
* @param int $id
* @return Response
* @throws UserUpdateException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function update(Request $request, $id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->validate($request, [
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array'
'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
]);
$user = $this->user->findOrFail($id);
$user = $this->userRepo->getById($id);
$user->fill($request->all());
// Role updates
if (userCan('users-manage') && $request->filled('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
$this->userRepo->setUserRoles($user, $roles);
}
// Password updates
@ -167,10 +172,23 @@ class UserController extends Controller
}
}
// Save profile image if in request
if ($request->has('profile_image')) {
$imageUpload = $request->file('profile_image');
$this->imageRepo->destroyImage($user->avatar);
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
$user->image_id = $image->id;
}
// Delete the profile image if set to
if ($request->has('profile_image_reset')) {
$this->imageRepo->destroyImage($user->avatar);
}
$user->save();
session()->flash('success', trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
return redirect($redirectUrl);
}
@ -181,26 +199,23 @@ class UserController extends Controller
*/
public function delete($id)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->user->findOrFail($id);
$user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]);
return view('users.delete', ['user' => $user]);
}
/**
* Remove the specified user from storage.
* @param int $id
* @return Response
* @throws \Exception
*/
public function destroy($id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
@ -228,10 +243,12 @@ class UserController extends Controller
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users/profile', [
return view('users.profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
@ -247,19 +264,7 @@ class UserController extends Controller
*/
public function switchBookView($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->user->findOrFail($id);
setting()->putUser($user, 'books_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
return $this->switchViewType($id, $request, 'books');
}
/**
@ -270,18 +275,97 @@ class UserController extends Controller
*/
public function switchShelfView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
return $this->switchViewType($id, $request, 'bookshelves');
}
/**
* For a type of list, switch with stored view type for a user.
* @param integer $userId
* @param Request $request
* @param string $listName
* @return \Illuminate\Http\RedirectResponse
*/
protected function switchViewType($userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'bookshelves_view_type', $viewType);
$user = $this->userRepo->getById($userId);
$key = $listName . '_view_type';
setting()->putUser($user, $key, $viewType);
return redirect()->back(302, [], "/settings/users/$id");
return redirect()->back(302, [], "/settings/users/$userId");
}
/**
* Change the stored sort type for a particular view.
* @param string $id
* @param string $type
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function changeSort(string $id, string $type, Request $request)
{
$validSortTypes = ['books', 'bookshelves'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
return $this->changeListSort($id, $request, $type);
}
/**
* Update the stored section expansion preference for the given user.
* @param string $id
* @param string $key
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function updateExpansionPreference(string $id, string $key, Request $request)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
if (!in_array($key, $keyWhitelist)) {
return response("Invalid key", 500);
}
$newState = $request->get('expand', 'false');
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
return response("", 204);
}
/**
* Changed the stored preference for a list sort order.
* @param int $userId
* @param Request $request
* @param string $listName
* @return \Illuminate\Http\RedirectResponse
*/
protected function changeListSort(int $userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$sort = $request->get('sort');
if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
$sort = 'name';
}
$order = $request->get('order');
if (!in_array($order, ['asc', 'desc'])) {
$order = 'asc';
}
$user = $this->user->findOrFail($userId);
$sortKey = $listName . '_sort';
$orderKey = $listName . '_sort_order';
setting()->putUser($user, $sortKey, $sort);
setting()->putUser($user, $orderKey, $order);
return redirect()->back(302, [], "/settings/users/$userId");
}
}

View File

@ -37,7 +37,7 @@ class Authenticate
}
}
if ($this->auth->guest() && !setting('app-public')) {
if (!hasAppAccess()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {

View File

@ -7,8 +7,40 @@ use Illuminate\Http\Request;
class Localization
{
/**
* Array of right-to-left locales
* @var array
*/
protected $rtlLocales = ['ar'];
/**
* Map of BookStack locale names to best-estimate system locale names.
* @var array
*/
protected $localeMap = [
'ar' => 'ar',
'de' => 'de_DE',
'de_informal' => 'de_DE',
'en' => 'en_GB',
'es' => 'es_ES',
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'it' => 'it_IT',
'ja' => 'ja',
'kr' => 'ko_KR',
'nl' => 'nl_NL',
'pl' => 'pl_PL',
'pt_BR' => 'pt_BR',
'pt_BR' => 'pt_BR',
'ru' => 'ru',
'sk' => 'sk_SK',
'sv' => 'sv_SE',
'uk' => 'uk_UA',
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
];
/**
* Handle an incoming request.
*
@ -19,6 +51,7 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
config()->set('app.default_locale', $defaultLang);
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
@ -33,6 +66,7 @@ class Localization
app()->setLocale($locale);
Carbon::setLocale($locale);
$this->setSystemDateLocale($locale);
return $next($request);
}
@ -53,4 +87,18 @@ class Localization
}
return $default;
}
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
* @param string $locale
*/
protected function setSystemDateLocale(string $locale)
{
$systemLocale = $this->localeMap[$locale] ?? $locale;
$set = setlocale(LC_TIME, $systemLocale);
if ($set === false) {
setlocale(LC_TIME, $systemLocale . '.utf8');
}
}
}

View File

@ -31,5 +31,4 @@ class MailNotification extends Notification implements ShouldQueue
'text' => 'vendor.notifications.email-plain'
]);
}
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Notifications;
class ResetPassword extends MailNotification
{
/**

View File

@ -3,11 +3,14 @@
use Blade;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Schema;
use Validator;
@ -22,9 +25,14 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
$validImageExtensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff', 'webp'];
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
});
Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
$uploadName = $value->getClientOriginalName();
return substr_count($uploadName, '.') < 2;
});
// Custom blade view directives
@ -42,6 +50,9 @@ class AppServiceProvider extends ServiceProvider
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
]);
// View Composers
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
}
/**

View File

@ -2,19 +2,11 @@
namespace BookStack\Providers;
use BookStack\Actions\Activity;
use BookStack\Actions\ActivityService;
use BookStack\Actions\View;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
class CustomFacadeProvider extends ServiceProvider
{
@ -36,33 +28,19 @@ class CustomFacadeProvider extends ServiceProvider
public function register()
{
$this->app->bind('activity', function () {
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
return $this->app->make(ActivityService::class);
});
$this->app->bind('views', function () {
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
return $this->app->make(ViewService::class);
});
$this->app->bind('setting', function () {
return new SettingService(
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
return $this->app->make(SettingService::class);
});
$this->app->bind('images', function () {
return new ImageService(
$this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class)
);
return $this->app->make(ImageService::class);
});
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Providers;
use BookStack\Translation\Translator;
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
@ -29,4 +28,4 @@ class TranslationServiceProvider extends \Illuminate\Translation\TranslationServ
return $trans;
});
}
}
}

View File

@ -41,6 +41,7 @@ class SettingService
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
@ -60,9 +61,23 @@ class SettingService
*/
public function getUser($user, $key, $default = false)
{
if ($user->isDefault()) {
return session()->get($key, $default);
}
return $this->get($this->userKey($user->id, $key), $default);
}
/**
* Get a value for the current logged-in user.
* @param $key
* @param bool $default
* @return bool|string
*/
public function getForCurrentUser($key, $default = false)
{
return $this->getUser(user(), $key, $default);
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
@ -179,6 +194,9 @@ class SettingService
*/
public function putUser($user, $key, $value)
{
if ($user->isDefault()) {
return session()->put($key, $value);
}
return $this->put($this->userKey($user->id, $key), $value);
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
@ -70,5 +69,4 @@ class Translator extends \Illuminate\Translation\Translator
{
return $this->baseLocaleMap[$locale] ?? null;
}
}
}

View File

@ -44,7 +44,7 @@ class AttachmentService extends UploadService
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachmentPath = $this->putFileInStorage($uploadedFile);
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
$attachment = Attachment::forceCreate([
@ -75,7 +75,7 @@ class AttachmentService extends UploadService
}
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachmentPath = $this->putFileInStorage($uploadedFile);
$attachment->name = $attachmentName;
$attachment->path = $attachmentPath;
@ -174,19 +174,18 @@ class AttachmentService extends UploadService
/**
* Store a file in storage with the given filename
* @param $attachmentName
* @param UploadedFile $uploadedFile
* @return string
* @throws FileUploadException
*/
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
protected function putFileInStorage(UploadedFile $uploadedFile)
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
$uploadFileName = $attachmentName;
$uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($basePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}

View File

@ -0,0 +1,33 @@
<?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\Uploads;
use BookStack\Entities\Page;
use BookStack\Ownable;
use Images;
@ -20,4 +21,14 @@ class Image extends Ownable
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
}
/**
* Get the page this image has been uploaded to.
* Only applicable to gallery or drawio image types.
* @return Page|null
*/
public function getPage()
{
return $this->belongsTo(Page::class, 'uploaded_to')->first();
}
}

View File

@ -2,6 +2,7 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Page;
use Illuminate\Database\Eloquent\Builder;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo
@ -19,8 +20,12 @@ class ImageRepo
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @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
) {
$this->image = $image;
$this->imageService = $imageService;
$this->restrictionService = $permissionService;
@ -31,7 +36,7 @@ class ImageRepo
/**
* Get an image with the given id.
* @param $id
* @return mixed
* @return Image
*/
public function getById($id)
{
@ -44,95 +49,113 @@ class ImageRepo
* @param $query
* @param int $page
* @param int $pageSize
* @param bool $filterOnPage
* @return array
*/
private function returnPaginated($query, $page = 0, $pageSize = 24)
private function returnPaginated($query, $page = 1, $pageSize = 24)
{
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
$returnImages = $images->take(24);
$returnImages = $images->take($pageSize);
$returnImages->each(function ($image) {
$this->loadThumbs($image);
});
return [
'images' => $returnImages,
'hasMore' => $hasMore
'has_more' => $hasMore
];
}
/**
* Gets a load images paginated, filtered by image type.
* Fetch a list of images in a paginated format, filtered by image type.
* Can be filtered by uploaded to and also by name.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* @param int $uploadedTo
* @param string|null $search
* @param callable|null $whereClause
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
{
$images = $this->image->where('type', '=', strtolower($type));
public function getPaginatedByType(
string $type,
int $page = 0,
int $pageSize = 24,
int $uploadedTo = null,
string $search = null,
callable $whereClause = null
) {
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
if ($uploadedTo !== null) {
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
}
return $this->returnPaginated($images, $page, $pageSize);
if ($search !== null) {
$imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
}
// Filter by page access
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
if ($whereClause !== null) {
$imageQuery = $imageQuery->where($whereClause);
}
return $this->returnPaginated($imageQuery, $page, $pageSize);
}
/**
* Search for images by query, of a particular type.
* Get paginated gallery images within a specific page or book.
* @param string $type
* @param string $filterType
* @param int $page
* @param int $pageSize
* @param string $searchTerm
* @param int|null $uploadedTo
* @param string|null $search
* @return array
*/
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
}
public function getEntityFiltered(
string $type,
string $filterType = null,
int $page = 0,
int $pageSize = 24,
int $uploadedTo = null,
string $search = null
) {
$contextPage = $this->page->findOrFail($uploadedTo);
$parentFilter = null;
/**
* Get gallery images with a particular filter criteria such as
* being within the current book or page.
* @param $filter
* @param $pageId
* @param int $pageNum
* @param int $pageSize
* @return array
*/
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', 'gallery');
$page = $this->page->findOrFail($pageId);
if ($filter === 'page') {
$images = $images->where('uploaded_to', '=', $page->id);
} elseif ($filter === 'book') {
$validPageIds = $page->book->pages->pluck('id')->toArray();
$images = $images->whereIn('uploaded_to', $validPageIds);
if ($filterType === 'book' || $filterType === 'page') {
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') {
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
$query->whereIn('uploaded_to', $validPageIds);
}
};
}
return $this->returnPaginated($images, $pageNum, $pageSize);
return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);
}
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true)
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
$this->loadThumbs($image);
return $image;
}
@ -175,12 +198,27 @@ class ImageRepo
* @return bool
* @throws \Exception
*/
public function destroyImage(Image $image)
public function destroyImage(Image $image = null)
{
$this->imageService->destroy($image);
if ($image) {
$this->imageService->destroy($image);
}
return true;
}
/**
* Destroy all images of a certain type.
* @param string $imageType
* @throws \Exception
*/
public function destroyByType(string $imageType)
{
$images = $this->image->where('type', '=', $imageType)->get();
foreach ($images as $image) {
$this->destroyImage($image);
}
}
/**
* Load thumbnails onto an image object.
@ -191,8 +229,8 @@ class ImageRepo
protected function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150),
'display' => $this->getThumbnail($image, 840, 0, true)
'gallery' => $this->getThumbnail($image, 150, 150, false),
'display' => $this->getThumbnail($image, 840, null, true)
];
}
@ -208,7 +246,7 @@ class ImageRepo
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
@ -232,13 +270,11 @@ class ImageRepo
}
/**
* Check if the provided image type is valid.
* @param $type
* @return bool
* Get the validation rules for image files.
* @return string
*/
public function isValidType($type)
public function getImageValidationRules()
{
$validTypes = ['gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Uploads;
use BookStack\Auth\User;
use BookStack\Exceptions\HttpFetchException;
use BookStack\Exceptions\ImageUploadException;
use DB;
use Exception;
@ -8,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use phpDocumentor\Reflection\Types\Integer;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService extends UploadService
@ -17,6 +19,7 @@ class ImageService extends UploadService
protected $cache;
protected $storageUrl;
protected $image;
protected $http;
/**
* ImageService constructor.
@ -24,12 +27,14 @@ class ImageService extends UploadService
* @param ImageManager $imageTool
* @param FileSystem $fileSystem
* @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->imageTool = $imageTool;
$this->cache = $cache;
$this->http = $http;
parent::__construct($fileSystem);
}
@ -53,15 +58,29 @@ class ImageService extends UploadService
/**
* Saves a new image from an upload.
* @param UploadedFile $uploadedFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return mixed
* @throws ImageUploadException
*/
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
{
public function saveNewFromUpload(
UploadedFile $uploadedFile,
string $type,
int $uploadedTo = 0,
int $resizeWidth = null,
int $resizeHeight = null,
bool $keepRatio = true
) {
$imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath());
if ($resizeWidth !== null || $resizeHeight !== null) {
$imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio);
}
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
@ -95,8 +114,9 @@ class ImageService extends UploadService
private function saveNewFromUrl($url, $type, $imageName = false)
{
$imageName = $imageName ? $imageName : basename($url);
$imageData = file_get_contents($url);
if ($imageData === false) {
try {
$imageData = $this->http->fetch($url);
} catch (HttpFetchException $exception) {
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}
return $this->saveNew($imageName, $imageData, $type);
@ -117,7 +137,7 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
@ -196,8 +216,28 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath);
}
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
}
/**
* Resize image data.
* @param string $imageData
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* @throws ImageUploadException
*/
protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true)
{
try {
$thumb = $this->imageTool->make($storage->get($imagePath));
$thumb = $this->imageTool->make($imageData);
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
@ -206,20 +246,14 @@ class ImageService extends UploadService
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$thumb->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
} else {
$thumb->fit($width, $height);
}
$thumbData = (string)$thumb->encode();
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
return (string)$thumb->encode();
}
/**
@ -279,24 +313,58 @@ 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 \BookStack\Auth\User $user
* @param int $size
* @return mixed
* @return Image
* @throws Exception
*/
public function saveUserGravatar(User $user, $size = 500)
public function saveUserAvatar(User $user, $size = 500)
{
$emailHash = md5(strtolower(trim($user->email)));
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
$image = $this->saveNewFromUrl($url, 'user', $imageName);
$avatarUrl = $this->getAvatarUrl();
$email = strtolower(trim($user->email));
$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->updated_by = $user->id;
$image->uploaded_to = $user->id;
$image->save();
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.
@ -365,14 +433,7 @@ class ImageService extends UploadService
}
} else {
try {
$ch = curl_init();
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);
}
$imageData = $this->http->fetch($uri);
} catch (\Exception $e) {
}
}

View File

@ -1,5 +1,7 @@
<?php
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Ownable;
/**
@ -41,30 +43,52 @@ function user()
* Check if current user is a signed in user.
* @return bool
*/
function signedInUser()
function signedInUser() : bool
{
return auth()->user() && !auth()->user()->isDefault();
}
/**
* Check if the current user has general access.
* @return bool
*/
function hasAppAccess() : bool
{
return !auth()->guest() || setting('app-public');
}
/**
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against
* that particular item.
* @param $permission
* @param string $permission
* @param Ownable $ownable
* @return mixed
*/
function userCan($permission, Ownable $ownable = null)
function userCan(string $permission, Ownable $ownable = null)
{
if ($ownable === null) {
return user() && user()->can($permission);
}
// Check permission on ownable item
$permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
$permissionService = app(PermissionService::class);
return $permissionService->checkOwnableUserAccess($ownable, $permission);
}
/**
* Check if the current user has the given permission
* on any item in the system.
* @param string $permission
* @param string|null $entityClass
* @return bool
*/
function userCanOnAny(string $permission, string $entityClass = null)
{
$permissionService = app(PermissionService::class);
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
}
/**
* Helper to access system settings.
* @param $key

View File

@ -5,14 +5,18 @@
"license": "MIT",
"type": "project",
"require": {
"php": ">=7.0.0",
"php": ">=7.0.5",
"ext-json": "*",
"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",
"laravel/socialite": "^3.0",
"laravel/socialite": "3.0.x-dev",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",
@ -83,7 +87,7 @@
"optimize-autoloader": true,
"preferred-install": "dist",
"platform": {
"php": "7.0"
"php": "7.0.5"
}
}
}

286
composer.lock generated
View File

@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "4a15ede09baa575d7accd7a4f66067fd",
"content-hash": "0946a07729a7a1bfef9bac185a870afd",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.70.3",
"version": "3.86.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "8278052a097a4ebe2b798fab7e2e3c907bc01a47"
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8278052a097a4ebe2b798fab7e2e3c907bc01a47",
"reference": "8278052a097a4ebe2b798fab7e2e3c907bc01a47",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"shasum": ""
},
"require": {
@ -87,7 +87,7 @@
"s3",
"sdk"
],
"time": "2018-11-02T20:04:36+00:00"
"time": "2019-01-18T21:10:44+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
@ -729,30 +729,34 @@
},
{
"name": "dompdf/dompdf",
"version": "v0.8.2",
"version": "v0.8.3",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "5113accd9ae5d466077cce5208dcf3fb871bf8f6"
"reference": "75f13c700009be21a1965dc2c5b68a8708c22ba2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/5113accd9ae5d466077cce5208dcf3fb871bf8f6",
"reference": "5113accd9ae5d466077cce5208dcf3fb871bf8f6",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/75f13c700009be21a1965dc2c5b68a8708c22ba2",
"reference": "75f13c700009be21a1965dc2c5b68a8708c22ba2",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-gd": "*",
"ext-mbstring": "*",
"phenx/php-font-lib": "0.5.*",
"phenx/php-svg-lib": "0.3.*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "4.8.*",
"phpunit/phpunit": "^4.8|^5.5|^6.5",
"squizlabs/php_codesniffer": "2.*"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance"
},
"type": "library",
"extra": {
"branch-alias": {
@ -787,20 +791,20 @@
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"time": "2017-11-26T14:49:08+00:00"
"time": "2018-12-14T02:40:31+00:00"
},
{
"name": "egulias/email-validator",
"version": "2.1.6",
"version": "2.1.7",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "0578b32b30b22de3e8664f797cf846fc9246f786"
"reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0578b32b30b22de3e8664f797cf846fc9246f786",
"reference": "0578b32b30b22de3e8664f797cf846fc9246f786",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
"reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
"shasum": ""
},
"require": {
@ -844,7 +848,7 @@
"validation",
"validator"
],
"time": "2018-09-25T20:47:26+00:00"
"time": "2018-12-04T22:38:24+00:00"
},
{
"name": "erusev/parsedown",
@ -1116,32 +1120,33 @@
},
{
"name": "guzzlehttp/psr7",
"version": "1.4.2",
"version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
"reference": "9f83dded91781a01c63574e387eaa769be769115"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
"reference": "9f83dded91781a01c63574e387eaa769be769115",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0"
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
"dev-master": "1.5-dev"
}
},
"autoload": {
@ -1171,13 +1176,14 @@
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2017-03-20T17:10:46+00:00"
"time": "2018-12-04T20:46:45+00:00"
},
{
"name": "intervention/image",
@ -1451,16 +1457,16 @@
},
{
"name": "laravel/socialite",
"version": "v3.2.0",
"version": "3.0.x-dev",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481"
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/7194c0cd9fb2ce449669252b8ec316b85b7de481",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481",
"url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
"shasum": ""
},
"require": {
@ -1510,20 +1516,20 @@
"laravel",
"oauth"
],
"time": "2018-10-18T03:39:04+00:00"
"time": "2018-12-21T14:06:32+00:00"
},
{
"name": "league/flysystem",
"version": "1.0.48",
"version": "1.0.49",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "a6ded5b2f6055e2db97b4b859fdfca2b952b78aa"
"reference": "a63cc83d8a931b271be45148fa39ba7156782ffd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a6ded5b2f6055e2db97b4b859fdfca2b952b78aa",
"reference": "a6ded5b2f6055e2db97b4b859fdfca2b952b78aa",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a63cc83d8a931b271be45148fa39ba7156782ffd",
"reference": "a63cc83d8a931b271be45148fa39ba7156782ffd",
"shasum": ""
},
"require": {
@ -1594,7 +1600,7 @@
"sftp",
"storage"
],
"time": "2018-10-15T13:53:10+00:00"
"time": "2018-11-23T23:41:29+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
@ -1708,16 +1714,16 @@
},
{
"name": "monolog/monolog",
"version": "1.23.0",
"version": "1.24.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4"
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
"reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
"shasum": ""
},
"require": {
@ -1782,7 +1788,7 @@
"logging",
"psr-3"
],
"time": "2017-06-19T01:22:40+00:00"
"time": "2018-11-05T09:00:11+00:00"
},
{
"name": "mtdowling/cron-expression",
@ -1885,16 +1891,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.34.0",
"version": "1.36.2",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33"
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33",
"reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"shasum": ""
},
"require": {
@ -1902,9 +1908,12 @@
"symfony/translation": "~2.6 || ~3.0 || ~4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2",
"phpunit/phpunit": "^4.8.35 || ^5.7"
},
"suggest": {
"friendsofphp/php-cs-fixer": "Needed for the `composer phpcs` command. Allow to automatically fix code style.",
"phpstan/phpstan": "Needed for the `composer phpstan` command. Allow to detect potential errors."
},
"type": "library",
"extra": {
"laravel": {
@ -1936,7 +1945,7 @@
"datetime",
"time"
],
"time": "2018-09-20T19:36:25+00:00"
"time": "2018-12-28T10:07:33+00:00"
},
{
"name": "paragonie/random_compat",
@ -2211,16 +2220,16 @@
},
{
"name": "psr/log",
"version": "1.0.2",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
"reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
"url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"shasum": ""
},
"require": {
@ -2254,7 +2263,7 @@
"psr",
"psr-3"
],
"time": "2016-10-10T12:19:37+00:00"
"time": "2018-11-20T15:27:04+00:00"
},
{
"name": "psr/simple-cache",
@ -2304,6 +2313,46 @@
],
"time": "2017-10-23T01:57:42+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "2.0.5",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
"reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "~3.7.0",
"satooshi/php-coveralls": ">=1.0"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"time": "2016-02-11T07:05:27+00:00"
},
{
"name": "ramsey/uuid",
"version": "3.8.0",
@ -2506,20 +2555,20 @@
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.1",
"version": "v3.3.4",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7"
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
"shasum": ""
},
"require": {
"laravel/socialite": "~3.0",
"laravel/socialite": "~3.0|~4.0",
"php": "^5.6 || ^7.0"
},
"require-dev": {
@ -2536,8 +2585,7 @@
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/",
"SocialiteProviders\\Manager\\Test\\": "tests/"
"SocialiteProviders\\Manager\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -2548,10 +2596,14 @@
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"time": "2017-11-20T08:42:57+00:00"
"time": "2019-01-16T07:58:54+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@ -3615,16 +3667,16 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e"
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
"shasum": ""
},
"require": {
@ -3661,22 +3713,22 @@
"env",
"environment"
],
"time": "2018-07-29T20:33:41+00:00"
"time": "2018-10-30T17:29:25+00:00"
}
],
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.2.0",
"version": "v3.2.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "5b68f3972083a7eeec0d6f161962fcda71a127c0"
"reference": "9d5caf43c5f3a3aea2178942f281054805872e7c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/5b68f3972083a7eeec0d6f161962fcda71a127c0",
"reference": "5b68f3972083a7eeec0d6f161962fcda71a127c0",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/9d5caf43c5f3a3aea2178942f281054805872e7c",
"reference": "9d5caf43c5f3a3aea2178942f281054805872e7c",
"shasum": ""
},
"require": {
@ -3731,24 +3783,24 @@
"profiler",
"webprofiler"
],
"time": "2018-08-22T11:06:19+00:00"
"time": "2018-11-09T08:37:55+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
"version": "v2.5.1",
"version": "v2.5.3",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "7db1843473e1562d8e0490b51db847d3a1415140"
"reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/7db1843473e1562d8e0490b51db847d3a1415140",
"reference": "7db1843473e1562d8e0490b51db847d3a1415140",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/3d7f1240896a075aa23b13f82dfcbe165dadeef2",
"reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.0.4",
"barryvdh/reflection-docblock": "^2.0.6",
"composer/composer": "^1.6",
"illuminate/console": "^5.5,<5.8",
"illuminate/filesystem": "^5.5,<5.8",
@ -3805,20 +3857,20 @@
"phpstorm",
"sublime"
],
"time": "2018-09-06T18:41:09+00:00"
"time": "2018-12-19T12:12:05+00:00"
},
{
"name": "barryvdh/reflection-docblock",
"version": "v2.0.4",
"version": "v2.0.6",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/ReflectionDocBlock.git",
"reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c"
"reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/3dcbd98b5d9384a5357266efba8fd29884458e5c",
"reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c",
"url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/6b69015d83d3daf9004a71a89f26e27d27ef6a16",
"reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16",
"shasum": ""
},
"require": {
@ -3854,7 +3906,7 @@
"email": "mike.vanriel@naenius.com"
}
],
"time": "2016-06-13T19:28:20+00:00"
"time": "2018-12-13T10:34:14+00:00"
},
{
"name": "composer/ca-bundle",
@ -3914,16 +3966,16 @@
},
{
"name": "composer/composer",
"version": "1.7.3",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
"reference": "e965b9aaa8854c3067f1ed2ae45f436572d73eb7"
"reference": "d8aef3af866b28786ce9b8647e52c42496436669"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/composer/zipball/e965b9aaa8854c3067f1ed2ae45f436572d73eb7",
"reference": "e965b9aaa8854c3067f1ed2ae45f436572d73eb7",
"url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669",
"reference": "d8aef3af866b28786ce9b8647e52c42496436669",
"shasum": ""
},
"require": {
@ -3959,7 +4011,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
"dev-master": "1.8-dev"
}
},
"autoload": {
@ -3990,7 +4042,7 @@
"dependency",
"package"
],
"time": "2018-11-01T09:05:06+00:00"
"time": "2018-12-03T09:31:16+00:00"
},
{
"name": "composer/semver",
@ -4117,16 +4169,16 @@
},
{
"name": "composer/xdebug-handler",
"version": "1.3.0",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
"reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c"
"reference": "dc523135366eb68f22268d069ea7749486458562"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/b8e9745fb9b06ea6664d8872c4505fb16df4611c",
"reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562",
"reference": "dc523135366eb68f22268d069ea7749486458562",
"shasum": ""
},
"require": {
@ -4157,7 +4209,7 @@
"Xdebug",
"performance"
],
"time": "2018-08-31T19:07:57+00:00"
"time": "2018-11-29T10:59:02+00:00"
},
{
"name": "doctrine/instantiator",
@ -4374,23 +4426,23 @@
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.7",
"version": "5.2.8",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23"
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.1",
"friendsofphp/php-cs-fixer": "~2.2.20",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
@ -4436,7 +4488,7 @@
"json",
"schema"
],
"time": "2018-02-14T22:26:30+00:00"
"time": "2019-01-14T23:55:14+00:00"
},
{
"name": "laravel/browser-kit-testing",
@ -6020,16 +6072,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.3.2",
"version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e"
"reference": "379deb987e26c7cd103a7b387aea178baec96e48"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e",
"reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48",
"reference": "379deb987e26c7cd103a7b387aea178baec96e48",
"shasum": ""
},
"require": {
@ -6067,7 +6119,7 @@
"phpcs",
"standards"
],
"time": "2018-09-23T23:08:17+00:00"
"time": "2018-12-19T23:57:18+00:00"
},
{
"name": "symfony/dom-crawler",
@ -6216,20 +6268,21 @@
},
{
"name": "webmozart/assert",
"version": "1.3.0",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
"php": "^5.3.3 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
@ -6262,21 +6315,28 @@
"check",
"validate"
],
"time": "2018-01-29T19:49:41+00:00"
"time": "2018-12-25T11:19:39+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"laravel/socialite": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.0.0",
"php": ">=7.0.5",
"ext-json": "*",
"ext-tidy": "*",
"ext-dom": "*"
"ext-dom": "*",
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*",
"ext-curl": "*"
},
"platform-dev": [],
"platform-overrides": {
"php": "7.0"
"php": "7.0.5"
}
}

View File

@ -1,175 +1,85 @@
<?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 [
// The environment to run BookStack in.
// Options: production, development, demo, testing
'env' => env('APP_ENV', 'production'),
/**
* Set the default view type for various lists. Can be overridden by user preferences.
* This will be used for public viewers and users that have not set a preference.
*/
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list')
],
/**
* The number of revisions to keep in the database.
* Once this limit is reached older revisions will be deleted.
* If set to false then a limit will not be enforced.
*/
'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),
/**
* Override the default behaviour for allowing crawlers to crawl the instance.
* May be ignored if view has be overridden or modified.
* Defaults to null since, if not set, 'app-public' status used instead.
*/
'allow_robots' => env('ALLOW_ROBOTS', null),
/*
|--------------------------------------------------------------------------
| 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.
|
*/
// Enter the application in debug mode.
// Shows much more verbose error messages. Has potential to show
// private configuration variables so should remain disabled in public.
'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.
|
*/
// 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' => [
'books' => env('APP_VIEWS_BOOKS', 'list'),
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
],
// The number of revisions to keep in the database.
// Once this limit is reached older revisions will be deleted.
// If set to false then a limit will not be enforced.
'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),
// Override the default behaviour for allowing crawlers to crawl the instance.
// May be ignored if view has be overridden or modified.
// Defaults to null since, if not set, 'app-public' status used instead.
'allow_robots' => env('ALLOW_ROBOTS', null),
// Application Base URL, Used by laravel in development commands
// and used by BookStack in URL generation.
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
/*
|--------------------------------------------------------------------------
| 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',
/*
|--------------------------------------------------------------------------
| 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.
|
*/
// Application timezone for back-end date functions.
'timezone' => env('APP_TIMEZONE', 'UTC'),
// Default locale to use
'locale' => env('APP_LANG', 'en'),
'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'],
/*
|--------------------------------------------------------------------------
| Right-to-left text control
|--------------------------------------------------------------------------
|
| Right-to-left text control is set to false by default since English
| is the primary supported application but this may be dynamically
| altered by the applications localization system.
|
*/
'rtl' => false,
/*
|--------------------------------------------------------------------------
| 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),
/*
|--------------------------------------------------------------------------
| 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.
|
*/
// Locales available
'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
// Application Fallback Locale
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| 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!
|
*/
// Enable right-to-left text control.
'rtl' => false,
// 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'),
// Encryption cipher
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| 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"
|
*/
// Logging configuration
// Options: single, daily, syslog, errorlog
'log' => env('APP_LOGGING', 'single'),
/*
|--------------------------------------------------------------------------
| 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.
|
*/
// Application Services Provides
'providers' => [
/*
* Laravel Framework Service Providers...
*/
// Laravel Framework Service Providers...
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
@ -192,20 +102,17 @@ return [
Illuminate\Notifications\NotificationServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
/**
* Third Party
*/
// Third party service providers
Intervention\Image\ImageServiceProvider::class,
Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\Snappy\ServiceProvider::class,
/*
* Application Service Providers...
*/
// BookStack replacement service providers (Extends Laravel)
BookStack\Providers\PaginationServiceProvider::class,
BookStack\Providers\TranslationServiceProvider::class,
// BookStack custom service providers
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\BroadcastServiceProvider::class,
@ -225,8 +132,10 @@ return [
|
*/
// Class aliases, Registered on application start
'aliases' => [
// Laravel
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
@ -262,18 +171,12 @@ return [
'View' => Illuminate\Support\Facades\View::class,
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
/**
* Third Party
*/
// Third Party
'ImageTool' => Intervention\Image\Facades\Image::class,
'DomPDF' => Barryvdh\DomPDF\Facade::class,
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
/**
* Custom
*/
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Setting' => BookStack\Facades\Setting::class,
'Views' => BookStack\Facades\Views::class,
@ -281,6 +184,7 @@ return [
],
// Proxy configuration
'proxies' => env('APP_PROXIES', ''),
];

View File

@ -1,43 +1,32 @@
<?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 [
// Method of authentication to use
// Options: standard, ldap
'method' => env('AUTH_METHOD', 'standard'),
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| 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.
|
*/
// Authentication Defaults
// This option controls the default authentication "guard" and password
// reset options for your application.
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| 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"
|
*/
// Authentication Guards
// 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' => [
'web' => [
'driver' => 'session',
@ -50,23 +39,11 @@ return [
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| 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"
|
*/
// User Providers
// 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: database, eloquent, ldap
'providers' => [
'users' => [
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
@ -79,25 +56,10 @@ return [
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| 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.
|
*/
// Resetting Passwords
// 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' => [
'users' => [
'provider' => 'users',

View File

@ -1,31 +1,25 @@
<?php
/**
* Broadcasting 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 [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
*/
// Default Broadcaster
// This option controls the default broadcaster that will be used by the
// framework when an event needs to be broadcast. This can be set to
// any of the connections defined in the "connections" array below.
'default' => env('BROADCAST_DRIVER', 'pusher'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
// Broadcast Connections
// Here you may define all of the broadcast connections that will be used
// to broadcast events to other systems or over websockets. Samples of
// each available type of connection are provided inside this array.
'connections' => [
'pusher' => [

View File

@ -1,5 +1,13 @@
<?php
/**
* Caching 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.
*/
// MEMCACHED - Split out configuration into an array
if (env('CACHE_DRIVER') === 'memcached') {
$memcachedServerKeys = ['host', 'port', 'weight'];
@ -14,30 +22,11 @@ if (env('CACHE_DRIVER') === 'memcached') {
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache connection that gets used while
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
*/
// Default cache store to use
// Can be overridden at cache call-time
'default' => env('CACHE_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
*/
// Available caches stores
'stores' => [
'apc' => [
@ -71,17 +60,8 @@ return [
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing a RAM based store such as APC or Memcached, there might
| be other applications utilizing the same cache. So, we'll specify a
| value to get prefixed to all our keys so we can avoid collisions.
|
*/
// Cache key prefix
// Used to prevent collisions in shared cache systems.
'prefix' => env('CACHE_PREFIX', 'bookstack'),
];

View File

@ -1,35 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Additional Compiled Classes
|--------------------------------------------------------------------------
|
| Here you may specify additional classes to include in the compiled file
| generated by the `artisan optimize` command. These should be classes
| that are included on basically every request into the application.
|
*/
'files' => [
//
],
/*
|--------------------------------------------------------------------------
| Compiled File Providers
|--------------------------------------------------------------------------
|
| Here you may list service providers which define a "compiles" function
| that returns additional files that should be compiled, providing an
| easy way to get common files from any packages you are utilizing.
|
*/
'providers' => [
//
],
];

View File

@ -1,21 +1,46 @@
<?php
// REDIS - Split out configuration into an array
/**
* Database 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.
*/
// REDIS
// Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisServerKeys = ['host', 'port', 'database'];
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
$redisConfig = [];
$cluster = count($redisServers) > 1;
if ($cluster) {
$redisConfig['clusters'] = ['default' => []];
}
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
$serverConfig = [];
$configIndex = 0;
foreach ($redisDefaults as $configKey => $configDefault) {
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
$configIndex++;
}
if ($cluster) {
$redisConfig['clusters']['default'][] = $serverConfig;
} else {
$redisConfig['default'] = $serverConfig;
}
}
}
// MYSQL
// Split out port from host if set
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);
@ -26,48 +51,12 @@ if (count($mysql_host_exploded) > 1) {
return [
/*
|--------------------------------------------------------------------------
| PDO Fetch Style
|--------------------------------------------------------------------------
|
| By default, database results will be returned as instances of the PHP
| stdClass object; however, you may desire to retrieve records in an
| array format for simplicity. Here you can tweak the fetch style.
|
*/
'fetch' => PDO::FETCH_CLASS,
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
// Default database connection name.
// Options: mysql, mysql_testing
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
// Available database connections
// Many of those shown here are unsupported by BookStack.
'connections' => [
'sqlite' => [
@ -82,11 +71,13 @@ return [
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'port' => $mysql_port,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => false,
'engine' => null,
],
'mysql_testing' => [
@ -124,30 +115,13 @@ return [
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
// Migration Repository Table
// This table keeps track of all the migrations that have already run for
// your application. Using this information, we can determine which of
// the migrations on disk haven't actually been run in the database.
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
// Redis configuration to use if set
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
];

View File

@ -1,16 +1,16 @@
<?php
/**
* DOMPDF 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 [
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => [

View File

@ -1,57 +1,29 @@
<?php
/**
* Filesystem 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 [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. A "local" driver, as well as a variety of cloud
| based drivers are available for your choosing. Just store away!
|
| Supported: "local", "ftp", "s3", "rackspace"
|
*/
// Default Filesystem Disk
// Options: local, local_secure, s3
'default' => env('STORAGE_TYPE', 'local'),
/*
|--------------------------------------------------------------------------
| Storage URL
|--------------------------------------------------------------------------
|
| This is the url to where the storage is located for when using an external
| file storage service, such as s3, to store publicly accessible assets.
|
*/
// Storage URL
// This is the url to where the storage is located for when using an external
// file storage service, such as s3, to store publicly accessible assets.
'url' => env('STORAGE_URL', false),
/*
|--------------------------------------------------------------------------
| Default Cloud Filesystem Disk
|--------------------------------------------------------------------------
|
| Many applications store files both locally and in the cloud. For this
| reason, you may specify a default "cloud" driver here. This driver
| will be bound as the Cloud disk implementation in the container.
|
*/
// Default Cloud Filesystem Disk
'cloud' => 's3',
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Here you may configure as many filesystem "disks" as you wish, and you
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
*/
// Available filesystem disks
// Only local, local_secure & s3 are supported by BookStack
'disks' => [
'local' => [
@ -69,13 +41,6 @@ return [
'host' => 'ftp.example.com',
'username' => 'your-username',
'password' => 'your-password',
// Optional FTP Settings...
// 'port' => 21,
// 'root' => '',
// 'passive' => true,
// 'ssl' => true,
// 'timeout' => 30,
],
's3' => [
@ -84,6 +49,8 @@ return [
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
'region' => env('STORAGE_S3_REGION', 'your-region'),
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
],
'rackspace' => [

View File

@ -1,123 +1,44 @@
<?php
/**
* Mail 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 [
/*
|--------------------------------------------------------------------------
| Mail Driver
|--------------------------------------------------------------------------
|
| Laravel supports both SMTP and PHP's "mail" function as drivers for the
| sending of e-mail. You may specify which one you're using throughout
| your application here. By default, Laravel is setup for SMTP mail.
|
| Supported: "smtp", "mail", "sendmail", "mailgun", "mandrill", "ses", "log"
|
*/
// Mail driver to use.
// Options: smtp, mail, sendmail, log
'driver' => env('MAIL_DRIVER', 'smtp'),
/*
|--------------------------------------------------------------------------
| SMTP Host Address
|--------------------------------------------------------------------------
|
| Here you may provide the host address of the SMTP server used by your
| applications. A default option is provided that is compatible with
| the Mailgun mail service which will provide reliable deliveries.
|
*/
// SMTP host address
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
/*
|--------------------------------------------------------------------------
| SMTP Host Port
|--------------------------------------------------------------------------
|
| This is the SMTP port used by your application to deliver e-mails to
| users of the application. Like the host we have set this value to
| stay compatible with the Mailgun e-mail application by default.
|
*/
// SMTP host port
'port' => env('MAIL_PORT', 587),
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => ['address' => env('MAIL_FROM', 'mail@bookstackapp.com'), 'name' => env('MAIL_FROM_NAME','BookStack')],
/*
|--------------------------------------------------------------------------
| E-Mail Encryption Protocol
|--------------------------------------------------------------------------
|
| Here you may specify the encryption protocol that should be used when
| the application send e-mail messages. A sensible default using the
| transport layer security protocol should provide great security.
|
*/
// Global "From" address & name
'from' => [
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
'name' => env('MAIL_FROM_NAME','BookStack')
],
// Email encryption protocol
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
/*
|--------------------------------------------------------------------------
| SMTP Server Username
|--------------------------------------------------------------------------
|
| If your SMTP server requires a username for authentication, you should
| set it here. This will get used to authenticate with your server on
| connection. You may also set the "password" value below this one.
|
*/
// SMTP server username
'username' => env('MAIL_USERNAME'),
/*
|--------------------------------------------------------------------------
| SMTP Server Password
|--------------------------------------------------------------------------
|
| Here you may set the password required by your SMTP server to send out
| messages from your application. This will be given to the server on
| connection so that the application will be able to send messages.
|
*/
// SMTP server password
'password' => env('MAIL_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Sendmail System Path
|--------------------------------------------------------------------------
|
| When using the "sendmail" driver to send e-mails, we will need to know
| the path to where Sendmail lives on this server. A default path has
| been provided here, which will work well on most of your systems.
|
*/
// Sendmail application path
'sendmail' => '/usr/sbin/sendmail -bs',
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
// Email markdown configuration
'markdown' => [
'theme' => 'default',
'paths' => [

View File

@ -1,34 +1,20 @@
<?php
/**
* Queue 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 [
/*
|--------------------------------------------------------------------------
| Default Queue Driver
|--------------------------------------------------------------------------
|
| The Laravel queue API supports a variety of back-ends via an unified
| API, giving you convenient access to each back-end using the same
| syntax for each one. Here you may set the default queue driver.
|
| Supported: "null", "sync", "database", "beanstalkd",
| "sqs", "iron", "redis"
|
*/
// Default driver to use for the queue
// Options: null, sync, redis
'default' => env('QUEUE_DRIVER', 'sync'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection information for each server that
| is used by your application. A default configuration has been added
| for each back-end shipped with Laravel. You are free to add more.
|
*/
// Queue connection configuration
'connections' => [
'sync' => [
@ -75,17 +61,7 @@ return [
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control which database and table are used to store the jobs that
| have failed. You may change them to any database / table you wish.
|
*/
// Failed queue job logging
'failed' => [
'database' => 'mysql', 'table' => 'failed_jobs',
],

View File

@ -1,25 +1,25 @@
<?php
/**
* Third party service 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 [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Stripe, Mailgun, Mandrill, and others. This file provides a sane
| default location for this type of information, allowing packages
| to have a conventional place to find your various credentials.
|
*/
// Single option to disable non-auth external services such as Gravatar and Draw.io
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
'gravatar' => env('GRAVATAR', !env('DISABLE_EXTERNAL_SERVICES', false)),
// Draw.io integration active
'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)),
// URL for fetching avatars
'avatar_url' => env('AVATAR_URL', ''),
// Callback URL for social authentication methods
'callback_url' => env('APP_URL', false),
'mailgun' => [
@ -27,10 +27,6 @@ return [
'secret' => '',
],
'mandrill' => [
'secret' => '',
],
'ses' => [
'key' => '',
'secret' => '',
@ -145,10 +141,12 @@ return [
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
]
];

View File

@ -1,181 +1,80 @@
<?php
/**
* Session 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 [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option controls the default session "driver" that will be used on
| requests. By default, we will use the lightweight native driver but
| you may specify any of the other wonderful drivers provided here.
|
| Supported: "file", "cookie", "database", "apc",
| "memcached", "redis", "array"
|
*/
// Default session driver
// Options: file, cookie, database, redis, memcached, array
'driver' => env('SESSION_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
// Session lifetime, in minutes
'lifetime' => env('SESSION_LIFETIME', 120),
// Expire session on browser close
'expire_on_close' => false,
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it is stored. All encryption will be run
| automatically by Laravel and you can use the Session like normal.
|
*/
// Encrypt session data
'encrypt' => false,
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When using the native session driver, we need a location where session
| files may be stored. A default has been set for you but a different
| location may be specified. This is only needed for file sessions.
|
*/
// Location to store session files
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
// Session Database Connection
// When using the "database" or "redis" session drivers, you can specify a
// connection that should be used to manage these sessions. This should
// correspond to a connection in your database configuration options.
'connection' => null,
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table we
| should use to manage the sessions. Of course, a sensible default is
| provided for you; however, you are free to change this as needed.
|
*/
// Session database table, if database driver is in use
'table' => 'sessions',
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
// Session Sweeping Lottery
// Some session drivers must manually sweep their storage location to get
// rid of old sessions from storage. Here are the chances that it will
// happen on a given request. By default, the odds are 2 out of 100.
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the cookie used to identify a session
| instance by ID. The name specified here will get used every time a
| new session cookie is created by the framework for every driver.
|
*/
// Session Cookie Name
// Here you may change the name of the cookie used to identify a session
// instance by ID. The name specified here will get used every time a
// new session cookie is created by the framework for every driver.
'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application but you are free to change this when necessary.
|
*/
// Session Cookie Path
// The session cookie path determines the path for which the cookie will
// be regarded as available. Typically, this will be the root path of
// your application but you are free to change this when necessary.
'path' => '/',
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| Here you may change the domain of the cookie used to identify a session
| in your application. This will determine which domains the cookie is
| available to in your application. A sensible default has been set.
|
*/
// Session Cookie Domain
// Here you may change the domain of the cookie used to identify a session
// in your application. This will determine which domains the cookie is
// available to in your application. A sensible default has been set.
'domain' => env('SESSION_DOMAIN', null),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you if it can not be done securely.
|
*/
// HTTPS Only Cookies
// By setting this option to true, session cookies will only be sent back
// to the server if the browser has a HTTPS connection. This will keep
// the cookie from being sent to you if it can not be done securely.
'secure' => env('SESSION_SECURE_COOKIE', false),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. You are free to modify this option if needed.
|
*/
// HTTP Access Only
// Setting this value to true will prevent JavaScript from accessing the
// value of the cookie and the cookie will only be accessible through the HTTP protocol.
'http_only' => true,
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| do not enable this as other CSRF protection services are in place.
|
| Supported: "lax", "strict"
|
*/
// Same-Site Cookies
// This option determines how your cookies behave when cross-site requests
// take place, and can be used to mitigate CSRF attacks. By default, we
// do not enable this as other CSRF protection services are in place.
// Options: lax, strict
'same_site' => null,
];

View File

@ -1,17 +1,22 @@
<?php
/**
* The defaults for the system settings that are saved in the database.
* Default system settings.
*
* 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 [
'app-name' => 'BookStack',
'app-logo' => '',
'app-name-header' => true,
'app-editor' => 'wysiwyg',
'app-color' => '#0288D1',
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
'app-custom-head' => false,
'app-name' => 'BookStack',
'app-logo' => '',
'app-name-header' => true,
'app-editor' => 'wysiwyg',
'app-color' => '#0288D1',
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
'app-custom-head' => false,
'registration-enabled' => false,
];

View File

@ -1,5 +1,13 @@
<?php
/**
* SnappyPDF 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 [
'pdf' => [
'enabled' => true,

View File

@ -1,5 +1,14 @@
<?php
/**
* View 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.
*/
// Join up possible view locations
$viewPaths = [realpath(base_path('resources/views'))];
if ($theme = env('APP_THEME', false)) {
array_unshift($viewPaths, base_path('themes/' . $theme));
@ -7,43 +16,22 @@ if ($theme = env('APP_THEME', false)) {
return [
/*
|--------------------------------------------------------------------------
| App theme
|--------------------------------------------------------------------------
|
| This option defines the theme to use for the application. When a theme
| is set there mush be a `themes/<theme_name>` folder to hold the
| custom theme overrides.
|
*/
// App theme
// This option defines the theme to use for the application. When a theme
// is set there must be a `themes/<theme_name>` folder to hold the
// custom theme overrides.
'theme' => env('APP_THEME', false),
/*
|--------------------------------------------------------------------------
| View Storage Paths
|--------------------------------------------------------------------------
|
| Most templating systems load templates from disk. Here you may specify
| an array of paths that should be checked for your views. Of course
| the usual Laravel view path has already been registered for you.
|
*/
// View Storage Paths
// Most templating systems load templates from disk. Here you may specify
// an array of paths that should be checked for your views. Of course
// the usual Laravel view path has already been registered for you.
'paths' => $viewPaths,
/*
|--------------------------------------------------------------------------
| Compiled View Path
|--------------------------------------------------------------------------
|
| This option determines where all the compiled Blade templates will be
| stored for your application. Typically, this is within the storage
| directory. However, as usual, you are free to change this value.
|
*/
// Compiled View Path
// This option determines where all the compiled Blade templates will be
// stored for your application. Typically, this is within the storage
// directory. However, as usual, you are free to change this value.
'compiled' => realpath(storage_path('framework/views')),
];

2991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,17 +13,17 @@
"@babel/core": "^7.1.6",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.6",
"autoprefixer": "^8.6.5",
"autoprefixer": "^9.4.7",
"babel-loader": "^8.0.4",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"css-loader": "^2.1.0",
"livereload": "^0.7.0",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.10.0",
"npm-run-all": "^4.1.5",
"postcss-loader": "^2.1.6",
"postcss-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.3.0",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2"
},

View File

@ -31,6 +31,7 @@
<env name="MAIL_DRIVER" value="log"/>
<env name="AUTH_METHOD" value="standard"/>
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
<env name="AVATAR_URL" value=""/>
<env name="LDAP_VERSION" value="3"/>
<env name="STORAGE_TYPE" value="local"/>
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>

View File

@ -1 +1 @@
!function(){"use strict";var n=function(t){var e=t,r=function(){return e};return{get:r,set:function(t){e=t},clone:function(){return n(r())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),r=function(t){return t.fire("RestoreDraft")},i=function(t){return t.fire("StoreDraft")},s=function(t){return t.fire("RemoveDraft")},e=function(t,e){return((t=/^(\d+)([ms]?)$/.exec(""+(t||e)))[2]?{s:1e3,m:6e4}[t[2]]:1)*parseInt(t,10)},u=function(t){return t.getParam("autosave_ask_before_unload",!0)},f=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,document.location.pathname)).replace(/\{query\}/g,document.location.search)).replace(/\{hash\}/g,document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t){return e(t.settings.autosave_interval,"30s")},l=function(t){return e(t.settings.autosave_retention,"20m")},m=function(t,e){var r=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+r+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+r+">|<br>$","i").test(e)},v=function(t){var e=parseInt(a.getItem(f(t)+"time"),10)||0;return!((new Date).getTime()-e>l(t)&&(d(t,!1),1))},d=function(t,e){var r=f(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&s(t)},D=function(t){var e=f(t);!m(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),i(t))},g=function(t){var e=f(t);v(t)&&(t.setContent(a.getItem(e+"draft"),{format:"raw"}),r(t))},y={isEmpty:m,hasDraft:v,removeDraft:d,storeDraft:D,restoreDraft:g,startStoreDraft:function(t,e){var r=c(t);e.get()||(setInterval(function(){t.removed||D(t)},r),e.set(!0))},restoreLastDraft:function(t){t.undoManager.transact(function(){g(t),d(t)}),t.focus()}},p=function(e,r){return function(){var t=Array.prototype.slice.call(arguments);return e.apply(null,[r].concat(t))}},h=function(t){return{hasDraft:p(y.hasDraft,t),storeDraft:p(y.storeDraft,t),restoreDraft:p(y.restoreDraft,t),removeDraft:p(y.removeDraft,t),isEmpty:p(y.isEmpty,t)}},_=tinymce.util.Tools.resolve("tinymce.EditorManager");_._beforeUnloadHandler=function(){var e;return o.each(_.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var b=function(t){window.onbeforeunload=_._beforeUnloadHandler},I=function(r,n){return function(t){var e=t.control;e.disabled(!y.hasDraft(r)),r.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!y.hasDraft(r))}),y.startStoreDraft(r,n)}},w=function(t,e){t.addButton("restoredraft",{title:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e)}),t.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e),context:"file"})};t.add("autosave",function(t){var e=n(!1);return b(t),w(t,e),h(t)})}();
!function(a){"use strict";var i=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return i(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=function(t,e){var n=t||e,r=/^(\d+)([ms]?)$/.exec(""+n);return(r[2]?{s:1e3,m:6e4}[r[2]]:1)*parseInt(n,10)},s=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,a.document.location.pathname)).replace(/\{query\}/g,a.document.location.search)).replace(/\{hash\}/g,a.document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t,e){var n=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+n+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(e)},f=function(t){var e=parseInt(r.getItem(s(t)+"time"),10)||0;return!((new Date).getTime()-e>u(t.settings.autosave_retention,"20m")&&(l(t,!1),1))},l=function(t,e){var n=s(t);r.removeItem(n+"draft"),r.removeItem(n+"time"),!1!==e&&t.fire("RemoveDraft")},m=function(t){var e=s(t);!c(t)&&t.isDirty()&&(r.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),r.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},v=function(t){var e=s(t);f(t)&&(t.setContent(r.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},d=function(t,e){var n=u(t.settings.autosave_interval,"30s");e.get()||(setInterval(function(){t.removed||m(t)},n),e.set(!0))},g=function(t){t.undoManager.transact(function(){v(t),l(t)}),t.focus()};function y(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var p=tinymce.util.Tools.resolve("tinymce.EditorManager");p._beforeUnloadHandler=function(){var e;return o.each(p.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var h=function(n,r){return function(t){var e=t.control;e.disabled(!f(n)),n.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!f(n))}),d(n,r)}};t.add("autosave",function(t){var e,n,r,o=i(!1);return a.window.onbeforeunload=p._beforeUnloadHandler,n=o,(e=t).addButton("restoredraft",{title:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n)}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n),context:"file"}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&v(t)}),{hasDraft:y(f,r=t),storeDraft:y(m,r),restoreDraft:y(v,r),removeDraft:y(l,r),isEmpty:y(c,r)}})}(window);

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!function(){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=function(e,n){e.fire("FullscreenStateChanged",{state:n})},g=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=document.body,u=document.documentElement,d=n.get(),a=function(){var e,n,t,i;g.setStyle(l,"height",(t=window,i=document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){g.unbind(window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),g.removeClass(s,"mce-fullscreen"),g.removeClass(u,"mce-fullscreen"),g.removeClass(r,"mce-fullscreen"),o=d.scrollPos,window.scrollTo(o.x,o.y),g.unbind(window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),m(e,!1);else{var f={scrollPos:(c=g.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",g.addClass(s,"mce-fullscreen"),g.addClass(u,"mce-fullscreen"),g.addClass(r,"mce-fullscreen"),g.bind(window,"resize",a),e.on("remove",h),a(),n.set(f),m(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}();
!function(m){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),g=function(e,n){e.fire("FullscreenStateChanged",{state:n})},w=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=m.document.body,u=m.document.documentElement,d=n.get(),a=function(){var e,n,t,i;w.setStyle(l,"height",(t=m.window,i=m.document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){w.unbind(m.window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),w.removeClass(s,"mce-fullscreen"),w.removeClass(u,"mce-fullscreen"),w.removeClass(r,"mce-fullscreen"),o=d.scrollPos,m.window.scrollTo(o.x,o.y),w.unbind(m.window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),g(e,!1);else{var f={scrollPos:(c=w.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",w.addClass(s,"mce-fullscreen"),w.addClass(u,"mce-fullscreen"),w.addClass(r,"mce-fullscreen"),w.bind(m.window,"resize",a),e.on("remove",h),a(),n.set(f),g(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}(window);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),c=tinymce.util.Tools.resolve("tinymce.EditorManager"),s=tinymce.util.Tools.resolve("tinymce.Env"),a=tinymce.util.Tools.resolve("tinymce.util.Delay"),y=tinymce.util.Tools.resolve("tinymce.util.Tools"),f=tinymce.util.Tools.resolve("tinymce.util.VK"),d=function(e){return e.getParam("tab_focus",e.getParam("tabfocus_elements",":prev,:next"))},m=t.DOM,n=function(e){e.keyCode!==f.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()},i=function(r){function e(n){var i,o,e,l;if(!(n.keyCode!==f.TAB||n.ctrlKey||n.altKey||n.metaKey||n.isDefaultPrevented())&&(1===(e=y.explode(d(r))).length&&(e[1]=e[0],e[0]=":prev"),o=n.shiftKey?":prev"===e[0]?u(-1):m.get(e[0]):":next"===e[1]?u(1):m.get(e[1]))){var t=c.get(o.id||o.name);o.id&&t?t.focus():a.setTimeout(function(){s.webkit||window.focus(),o.focus()},10),n.preventDefault()}function u(e){function t(t){return/INPUT|TEXTAREA|BUTTON/.test(t.tagName)&&c.get(n.id)&&-1!==t.tabIndex&&function e(t){return"BODY"===t.nodeName||"hidden"!==t.type&&"none"!==t.style.display&&"hidden"!==t.style.visibility&&e(t.parentNode)}(t)}if(o=m.select(":input:enabled,*[tabindex]:not(iframe)"),y.each(o,function(e,t){if(e.id===r.id)return i=t,!1}),0<e){for(l=i+1;l<o.length;l++)if(t(o[l]))return o[l]}else for(l=i-1;0<=l;l--)if(t(o[l]))return o[l];return null}}r.on("init",function(){r.inline&&m.setAttrib(r.getBody(),"tabIndex",null),r.on("keyup",n),s.gecko?r.on("keypress keydown",e):r.on("keydown",e)})};e.add("tabfocus",function(e){i(e)})}();
!function(c){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=tinymce.util.Tools.resolve("tinymce.EditorManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),y=tinymce.util.Tools.resolve("tinymce.util.Delay"),f=tinymce.util.Tools.resolve("tinymce.util.Tools"),d=tinymce.util.Tools.resolve("tinymce.util.VK"),m=function(e){return e.getParam("tab_focus",e.getParam("tabfocus_elements",":prev,:next"))},v=t.DOM,n=function(e){e.keyCode!==d.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()},i=function(r){function e(n){var i,o,e,l;if(!(n.keyCode!==d.TAB||n.ctrlKey||n.altKey||n.metaKey||n.isDefaultPrevented())&&(1===(e=f.explode(m(r))).length&&(e[1]=e[0],e[0]=":prev"),o=n.shiftKey?":prev"===e[0]?u(-1):v.get(e[0]):":next"===e[1]?u(1):v.get(e[1]))){var t=s.get(o.id||o.name);o.id&&t?t.focus():y.setTimeout(function(){a.webkit||c.window.focus(),o.focus()},10),n.preventDefault()}function u(e){function t(t){return/INPUT|TEXTAREA|BUTTON/.test(t.tagName)&&s.get(n.id)&&-1!==t.tabIndex&&function e(t){return"BODY"===t.nodeName||"hidden"!==t.type&&"none"!==t.style.display&&"hidden"!==t.style.visibility&&e(t.parentNode)}(t)}if(o=v.select(":input:enabled,*[tabindex]:not(iframe)"),f.each(o,function(e,t){if(e.id===r.id)return i=t,!1}),0<e){for(l=i+1;l<o.length;l++)if(t(o[l]))return o[l]}else for(l=i-1;0<=l;l--)if(t(o[l]))return o[l];return null}}r.on("init",function(){r.inline&&v.setAttrib(r.getBody(),"tabIndex",null),r.on("keyup",n),a.gecko?r.on("keypress keydown",e):r.on("keydown",e)})};e.add("tabfocus",function(e){i(e)})}(window);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -87,6 +87,7 @@
<glyph unicode="&#xe906;" glyph-name="reload" d="M889.68 793.68c-93.608 102.216-228.154 166.32-377.68 166.32-282.77 0-512-229.23-512-512h96c0 229.75 186.25 416 416 416 123.020 0 233.542-53.418 309.696-138.306l-149.696-149.694h352v352l-134.32-134.32zM928 448c0-229.75-186.25-416-416-416-123.020 0-233.542 53.418-309.694 138.306l149.694 149.694h-352v-352l134.32 134.32c93.608-102.216 228.154-166.32 377.68-166.32 282.77 0 512 229.23 512 512h-96z" />
<glyph unicode="&#xe907;" glyph-name="translate" d="M553.6 304l-118.4 118.4c80 89.6 137.6 195.2 172.8 304h137.6v92.8h-326.4v92.8h-92.8v-92.8h-326.4v-92.8h518.4c-32-89.6-80-176-147.2-249.6-44.8 48-80 99.2-108.8 156.8h-92.8c35.2-76.8 80-147.2 137.6-211.2l-236.8-233.6 67.2-67.2 233.6 233.6 144-144c3.2 0 38.4 92.8 38.4 92.8zM816 540.8h-92.8l-208-560h92.8l51.2 140.8h220.8l51.2-140.8h92.8l-208 560zM691.2 214.4l76.8 201.6 76.8-201.6h-153.6z" />
<glyph unicode="&#xe908;" glyph-name="drag" d="M576 896h128v-128h-128v128zM576 640h128v-128h-128v128zM320 640h128v-128h-128v128zM576 384h128v-128h-128v128zM320 384h128v-128h-128v128zM320 128h128v-128h-128v128zM576 128h128v-128h-128v128zM320 896h128v-128h-128v128z" />
<glyph unicode="&#xe909;" glyph-name="format-painter" d="M768 746.667v42.667c0 23.467-19.2 42.667-42.667 42.667h-512c-23.467 0-42.667-19.2-42.667-42.667v-170.667c0-23.467 19.2-42.667 42.667-42.667h512c23.467 0 42.667 19.2 42.667 42.667v42.667h42.667v-170.667h-426.667v-384c0-23.467 19.2-42.667 42.667-42.667h85.333c23.467 0 42.667 19.2 42.667 42.667v298.667h341.333v341.333h-128z" />
<glyph unicode="&#xe90b;" glyph-name="home" d="M1024 369.556l-512 397.426-512-397.428v162.038l512 397.426 512-397.428zM896 384v-384h-256v256h-256v-256h-256v384l384 288z" />
<glyph unicode="&#xe911;" glyph-name="books" d="M576.234 670.73l242.712 81.432 203.584-606.784-242.712-81.432zM0 64h256v704h-256v-704zM64 640h128v-64h-128v64zM320 64h256v704h-256v-704zM384 640h128v-64h-128v64z" />
<glyph unicode="&#xe914;" glyph-name="upload" d="M839.432 760.57c27.492-27.492 50.554-78.672 55.552-120.57h-318.984v318.984c41.898-4.998 93.076-28.060 120.568-55.552l142.864-142.862zM512 576v384h-368c-44 0-80-36-80-80v-864c0-44 36-80 80-80h672c44 0 80 36 80 80v560h-384zM576 192v-192h-192v192h-160l256 256 256-256h-160z" />

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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