Compare commits

..

6 Commits

Author SHA1 Message Date
abijeet
cc275c0b53 Refactored the code for ExportService to use DomDocument.
Fixes #883

Also handling more scenarios.
2019-01-27 15:53:51 +05:30
abijeet
8a2c13729e Merge branch 'master' into fix/video-export 2019-01-05 17:42:20 +05:30
Dan Brown
5f113f3f52 Simplified code a little and renamed dynamicText variable
Just to be a little clearer of what it is.
2018-07-06 12:38:25 +01:00
Dan Brown
27954d6bc6 Added test to cover HTML export re-write
Also altered video regex to be non-greedy to allow mulitple video
matches in a single document
2018-07-06 11:57:11 +01:00
Abijeet
5bee25d651 Fixes a few comments.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-07-02 00:39:33 +05:30
Abijeet
0d1db98289 Fixes issues with video tags in PDF, HTML and Text exports.
Closes: #883

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-07-02 00:31:35 +05:30
364 changed files with 9261 additions and 18253 deletions

View File

@@ -1,14 +1,11 @@
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
# Environment
APP_ENV=production
APP_DEBUG=false
APP_KEY=SomeRandomString
# 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
# 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
# Database details
DB_HOST=localhost
@@ -16,16 +13,84 @@ 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
# 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
# SMTP mail options
# 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
# Disable default services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# Use custom avatar service, Sets fetch URL
# Possible placeholders: ${hash} ${size} ${email}
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
# AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
# LDAP Settings
LDAP_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
# Set this option to disable LDAPS Certificate Verification
LDAP_TLS_INSECURE=false
# Mail settings
MAIL_DRIVER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# A full list of options can be found in the '.env.example.complete' file.
MAIL_FROM=null
MAIL_FROM_NAME=null

View File

@@ -1,227 +0,0 @@
# 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

6
.gitignore vendored
View File

@@ -5,10 +5,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
/public/css
/public/js
/public/bower
/public/build/
/storage/images

View File

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

View File

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

View File

@@ -80,44 +80,24 @@ class LdapService
public function getUserDetails($userName)
{
$emailAttr = $this->config['email_attribute'];
$displayNameAttr = $this->config['display_name_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
if ($user === null) {
return null;
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
return [
'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $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
*/
@@ -182,14 +162,25 @@ class LdapService
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// 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);
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) {
array_unshift($ldapServer, '');
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
/*
* Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
* the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
* per handle.
*/
if($this->config['tls_insecure']) {
$this->ldap->setOption(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
$serverDetails = $this->parseServerString($this->config['server']);
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
@@ -204,27 +195,6 @@ 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
@@ -329,10 +299,10 @@ class LdapService
$count = 0;
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
$count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
$count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
}
for ($i = 0; $i < $count; $i++) {
for ($i=0; $i<$count; $i++) {
$dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1);
if (!in_array($dnComponents[0], $ldapGroups)) {
$ldapGroups[] = $dnComponents[0];

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,39 +556,6 @@ 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.
@@ -645,13 +612,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);
@@ -704,7 +671,7 @@ class PermissionService
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return QueryBuilder
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
@@ -732,21 +699,18 @@ class PermissionService
}
/**
* Add conditions to a query to filter the selection to related entities
* where permissions are granted.
* @param $entityType
* Filters pages that are a direct relation to another item.
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
$pageMorphClass = $this->entityProvider->page->getMorphClass();
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
@@ -764,9 +728,7 @@ class PermissionService
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
$this->clean();
return $q;
}

View File

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

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'];
protected $fillable = ['name', 'email', 'image_id'];
/**
* The attributes excluded from the model's JSON form.
@@ -216,12 +216,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getShortName($chars = 8)
{
if (mb_strlen($this->name) <= $chars) {
if (strlen($this->name) <= $chars) {
return $this->name;
}
$splitName = explode(' ', $this->name);
if (mb_strlen($splitName[0]) <= $chars) {
if (strlen($splitName[0]) <= $chars) {
return $splitName[0];
}

View File

@@ -6,7 +6,6 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Images;
class UserRepo
@@ -49,7 +48,7 @@ class UserRepo
/**
* Get all the users with their permissions.
* @return Builder|static
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsers()
{
@@ -60,7 +59,7 @@ class UserRepo
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return Builder|static
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
@@ -198,7 +197,7 @@ class UserRepo
$user->delete();
// Delete user profile images
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
@@ -224,15 +223,16 @@ 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, $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)
'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);
})
];
}
@@ -247,7 +247,6 @@ 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),
];
}
@@ -257,7 +256,7 @@ class UserRepo
*/
public function getAllRoles()
{
return $this->role->newQuery()->orderBy('name', 'asc')->get();
return $this->role->all();
}
/**

View File

@@ -49,7 +49,7 @@ class CreateAdmin extends Command
if (empty($email)) {
$email = $this->ask('Please specify an email address for the new admin user');
}
if (mb_strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->error('Invalid email address provided');
}
@@ -61,7 +61,7 @@ class CreateAdmin extends Command
if (empty($name)) {
$name = $this->ask('Please specify an name for the new admin user');
}
if (mb_strlen($name) < 2) {
if (strlen($name) < 2) {
return $this->error('Invalid name provided');
}
@@ -69,7 +69,7 @@ class CreateAdmin extends Command
if (empty($password)) {
$password = $this->secret('Please specify a password for the new admin user');
}
if (mb_strlen($password) < 5) {
if (strlen($password) < 5) {
return $this->error('Invalid password provided, Must be at least 5 characters');
}

View File

@@ -38,7 +38,7 @@ class Book extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
@@ -69,15 +69,6 @@ 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
@@ -101,10 +92,10 @@ class Book extends Entity
* @param int $length
* @return string
*/
public function getExcerpt(int $length = 100)
public function getExcerpt($length = 100)
{
$description = $this->description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**

View File

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

@@ -1,34 +0,0 @@
<?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,10 +53,10 @@ class Chapter extends Entity
* @param int $length
* @return string
*/
public function getExcerpt(int $length = 100)
public function getExcerpt($length = 100)
{
$description = $this->text ?? $this->description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
@@ -67,13 +67,4 @@ 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,11 +102,6 @@ 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
@@ -223,20 +218,6 @@ 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

@@ -1,60 +0,0 @@
<?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,22 +85,5 @@ 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

@@ -2,9 +2,14 @@
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Uploads\ImageService;
use BookStack\Exceptions\ExportException;
class ExportService
{
protected $contentMatching = [
'video' => ["www.youtube.com", "player.vimeo.com", "www.dailymotion.com"],
'map' => ['maps.google.com']
];
protected $entityRepo;
protected $imageService;
@@ -74,16 +79,17 @@ class ExportService
/**
* Convert a page to a PDF file.
* @param Page $page
* @param bool $isTesting
* @return mixed|string
* @throws \Throwable
*/
public function pageToPdf(Page $page)
public function pageToPdf(Page $page, bool $isTesting = false)
{
$this->entityRepo->renderPage($page);
$html = view('pages/pdf', [
'page' => $page
])->render();
return $this->htmlToPdf($html);
return $this->htmlToPdf($html, $isTesting);
}
/**
@@ -124,12 +130,16 @@ class ExportService
/**
* Convert normal webpage HTML to a PDF.
* @param $html
* @param $isTesting
* @return string
* @throws \Exception
*/
protected function htmlToPdf($html)
protected function htmlToPdf($html, $isTesting = false)
{
$containedHtml = $this->containHtml($html);
$containedHtml = $this->containHtml($html, true);
if ($isTesting) {
return $containedHtml;
}
$useWKHTML = config('snappy.pdf.binary') !== false;
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
@@ -143,46 +153,64 @@ class ExportService
/**
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @param bool $isPDF
* @return mixed|string
* @throws \Exception
* @throws \BookStack\Exceptions\ExportException
*/
protected function containHtml($htmlContent)
protected function containHtml(string $htmlContent, bool $isPDF = false) : string
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
$dom = $this->getDOM($htmlContent);
if ($dom === false) {
throw new ExportException(trans('errors.dom_parse_error'));
}
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
}
$newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
$htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
// replace image src with base64 encoded image strings
$images = $dom->getElementsByTagName('img');
foreach ($images as $img) {
$base64String = $this->imageService->imageUriToBase64($img->getAttribute('src'));
if ($base64String !== null) {
$img->setAttribute('src', $base64String);
$dom->saveHTML($img);
}
}
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
// Replace image src with base64 encoded image strings
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
}
// replace all relative hrefs.
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (strpos(trim($href), 'http') !== 0) {
$newHref = url($href);
$link->setAttribute('href', $newHref);
$dom->saveHTML($link);
}
}
// Replace any relative links with system domain
return $htmlContent;
// replace all src in video, audio and iframe tags
$xmlDoc = new \DOMXPath($dom);
$srcElements = $xmlDoc->query('//video | //audio | //iframe');
foreach ($srcElements as $element) {
$element = $this->fixRelativeSrc($element);
$dom->saveHTML($element);
if ($isPDF) {
$src = $element->getAttribute('src');
$label = $this->getContentLabel($src);
$div = $dom->createElement('div');
$textNode = $dom->createTextNode($label);
$anchor = $dom->createElement('a');
$anchor->setAttribute('href', $src);
$anchor->textContent = $src;
$div->appendChild($textNode);
$div->appendChild($anchor);
$element->parentNode->replaceChild($div, $element);
}
}
return $dom->saveHTML();
}
/**
@@ -190,11 +218,43 @@ class ExportService
* This method filters any bad looking content to provide a nice final output.
* @param Page $page
* @return mixed
* @throws \BookStack\Exceptions\ExportException
*/
public function pageToPlainText(Page $page)
{
$html = $this->entityRepo->renderPage($page);
$text = strip_tags($html);
$dom = $this->getDom($html);
if ($dom === false) {
throw new ExportException(trans('errors.dom_parse_error'));
}
// handle anchor tags.
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (strpos(trim($href), 'http') !== 0) {
$newHref = url($href);
$link->setAttribute('href', $newHref);
}
$link->textContent = trim($link->textContent . " ($href)");
$dom->saveHTML();
}
$xmlDoc = new \DOMXPath($dom);
$srcElements = $xmlDoc->query('//video | //audio | //iframe | //img');
foreach ($srcElements as $element) {
$element = $this->fixRelativeSrc($element);
$fixedSrc = $element->getAttribute('src');
$label = $this->getContentLabel($fixedSrc);
$finalLabel = "\n\n$label $fixedSrc\n\n";
$textNode = $dom->createTextNode($finalLabel);
$element->parentNode->replaceChild($textNode, $element);
}
$text = strip_tags($dom->saveHTML());
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.
@@ -238,4 +298,37 @@ class ExportService
}
return $text;
}
protected function getDom(string $htmlContent) : \DOMDocument
{
// See - https://stackoverflow.com/a/17559716/903324
$dom = new \DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($htmlContent);
libxml_clear_errors();
return $dom;
}
protected function fixRelativeSrc(\DOMElement $element): \DOMElement
{
$src = $element->getAttribute('src');
if (strpos(trim($src), 'http') !== 0) {
$newSrc = 'https:' . $src;
$element->setAttribute('src', $newSrc);
}
return $element;
}
protected function getContentLabel(string $src) : string
{
foreach ($this->contentMatching as $key => $possibleValues) {
foreach ($possibleValues as $value) {
if (strpos($src, $value)) {
return trans("entities.$key");
}
}
}
return trans('entities.embedded_content');
}
}

View File

@@ -102,6 +102,17 @@ 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,6 +1,5 @@
<?php namespace BookStack\Entities\Repos;
use Activity;
use BookStack\Actions\TagRepo;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
@@ -16,13 +15,8 @@ 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
{
@@ -107,7 +101,7 @@ class EntityRepo
* @param integer $id
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return Entity
* @return \BookStack\Entities\Entity
*/
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
{
@@ -125,7 +119,7 @@ class EntityRepo
* @param []int $ids
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
*/
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
{
@@ -143,7 +137,7 @@ class EntityRepo
* @param string $type
* @param string $slug
* @param string|bool $bookSlug
* @return Entity
* @return \BookStack\Entities\Entity
* @throws NotFoundException
*/
public function getBySlug($type, $slug, $bookSlug = false)
@@ -185,38 +179,11 @@ class EntityRepo
* Get all entities in a paginated format
* @param $type
* @param int $count
* @param string $sort
* @param string $order
* @param null|callable $queryAddition
* @return LengthAwarePaginator
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
public function getAllPaginated($type, $count = 10)
{
$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;
return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
}
/**
@@ -298,14 +265,15 @@ class EntityRepo
/**
* Get the most popular entities base on all views.
* @param string $type
* @param string|bool $type
* @param int $count
* @param int $page
* @return mixed
*/
public function getPopular(string $type, int $count = 10, int $page = 0)
public function getPopular($type, $count = 10, $page = 0)
{
return $this->viewService->getPopular($count, $page, $type);
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
return $this->viewService->getPopular($count, $page, $filter);
}
/**
@@ -337,7 +305,7 @@ class EntityRepo
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Bookshelf $bookshelf
* @param \BookStack\Entities\Bookshelf $bookshelf
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getBookshelfChildren(Bookshelf $bookshelf)
@@ -345,23 +313,11 @@ 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 Book $book
* @param \BookStack\Entities\Book $book
* @param bool $filterDrafts
* @param bool $renderPages
* @return mixed
@@ -411,7 +367,7 @@ class EntityRepo
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Chapter $chapter
* @param \BookStack\Entities\Chapter $chapter
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getChapterChildren(Chapter $chapter)
@@ -423,7 +379,7 @@ class EntityRepo
/**
* Get the next sequential priority for a new child element in the given book.
* @param Book $book
* @param \BookStack\Entities\Book $book
* @return int
*/
public function getNewBookPriority(Book $book)
@@ -434,7 +390,7 @@ class EntityRepo
/**
* Get a new priority for a new page to be added to the given chapter.
* @param Chapter $chapter
* @param \BookStack\Entities\Chapter $chapter
* @return int
*/
public function getNewChapterPriority(Chapter $chapter)
@@ -483,8 +439,8 @@ class EntityRepo
/**
* Updates entity restrictions from a request
* @param Request $request
* @param Entity $entity
* @throws Throwable
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
*/
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
{
@@ -514,7 +470,7 @@ class EntityRepo
* @param string $type
* @param array $input
* @param bool|Book $book
* @return Entity
* @return \BookStack\Entities\Entity
*/
public function createFromInput($type, $input = [], $book = false)
{
@@ -538,9 +494,9 @@ class EntityRepo
* Update entity details from request input.
* Used for books and chapters
* @param string $type
* @param Entity $entityModel
* @param \BookStack\Entities\Entity $entityModel
* @param array $input
* @return Entity
* @return \BookStack\Entities\Entity
*/
public function updateFromInput($type, Entity $entityModel, $input = [])
{
@@ -563,7 +519,7 @@ class EntityRepo
/**
* Sync the books assigned to a shelf from a comma-separated list
* of book IDs.
* @param Bookshelf $shelf
* @param \BookStack\Entities\Bookshelf $shelf
* @param string $books
*/
public function updateShelfBooks(Bookshelf $shelf, string $books)
@@ -582,28 +538,13 @@ 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 Entity
* @return \BookStack\Entities\Entity
*/
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
{
@@ -658,47 +599,23 @@ class EntityRepo
}
/**
* Render the page for viewing
* Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page
* @param bool $blankIncludes
* @return string
* @param bool $ignorePermissions
* @return mixed|string
*/
public function renderPage(Page $page, bool $blankIncludes = false) : string
public function renderPage(Page $page, $ignorePermissions = false)
{
$content = $page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($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);
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) {
return $content;
}
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
@@ -708,23 +625,22 @@ class EntityRepo
continue;
}
$matchedPage = $this->getById('page', $pageId);
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
$html = str_replace($matches[0][$index], '', $html);
$content = str_replace($matches[0][$index], '', $content);
continue;
}
if (count($splitInclude) === 1) {
$html = str_replace($matches[0][$index], $matchedPage->html, $html);
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
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) {
$html = str_replace($matches[0][$index], '', $html);
$content = str_replace($matches[0][$index], '', $content);
continue;
}
$innerContent = '';
@@ -736,49 +652,29 @@ class EntityRepo
$innerContent .= $doc->saveHTML($childNode);
}
}
libxml_clear_errors();
$html = str_replace($matches[0][$index], trim($innerContent), $html);
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
return $html;
return $content;
}
/**
* Escape script tags within HTML content.
* @param string $html
* @return string
* @return mixed
*/
protected function escapeScripts(string $html) : string
protected function escapeScripts(string $html)
{
if ($html == '') {
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) {
return $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('//script');
foreach ($scriptElems as $scriptElem) {
$scriptElem->parentNode->removeChild($scriptElem);
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[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;
}
@@ -789,7 +685,7 @@ class EntityRepo
*/
public function searchForImage($imageString)
{
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
@@ -800,8 +696,8 @@ class EntityRepo
/**
* Destroy a bookshelf instance
* @param Bookshelf $shelf
* @throws Throwable
* @param \BookStack\Entities\Bookshelf $shelf
* @throws \Throwable
*/
public function destroyBookshelf(Bookshelf $shelf)
{
@@ -811,9 +707,9 @@ class EntityRepo
/**
* Destroy the provided book and all its child entities.
* @param Book $book
* @param \BookStack\Entities\Book $book
* @throws NotifyException
* @throws Throwable
* @throws \Throwable
*/
public function destroyBook(Book $book)
{
@@ -829,8 +725,8 @@ class EntityRepo
/**
* Destroy a chapter and its relations.
* @param Chapter $chapter
* @throws Throwable
* @param \BookStack\Entities\Chapter $chapter
* @throws \Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@@ -848,17 +744,14 @@ class EntityRepo
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
* @throws Throwable
* @throws \Throwable
*/
public function destroyPage(Page $page)
{
// Check if set as custom homepage & remove setting if not used or throw error if active
// Check if set as custom homepage
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
if (setting('app-homepage-type') === 'page') {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
setting()->remove('app-homepage');
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
$this->destroyEntityCommonRelations($page);
@@ -874,12 +767,12 @@ class EntityRepo
/**
* Destroy or handle the common relations connected to an entity.
* @param Entity $entity
* @throws Throwable
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
*/
protected function destroyEntityCommonRelations(Entity $entity)
{
Activity::removeEntity($entity);
\Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
@@ -891,9 +784,9 @@ class EntityRepo
/**
* Copy the permissions of a bookshelf to all child books.
* Returns the number of books that had permissions updated.
* @param Bookshelf $bookshelf
* @param \BookStack\Entities\Bookshelf $bookshelf
* @return int
* @throws Throwable
* @throws \Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{

View File

@@ -7,7 +7,6 @@ use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use Carbon\Carbon;
use DOMDocument;
use DOMElement;
use DOMXPath;
class PageRepo extends EntityRepo
@@ -130,7 +129,8 @@ 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,7 +139,6 @@ 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'));
@@ -148,17 +147,37 @@ class PageRepo extends EntityRepo
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Set ids on top-level nodes
$idMap = [];
foreach ($childNodes as $index => $childNode) {
$this->setUniqueId($childNode, $idMap);
}
// Ensure no duplicate ids are used
$idArray = [];
// Ensure no duplicate ids within child items
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
$this->setUniqueId($domElem, $idMap);
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') {
continue;
}
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
$idArray[] = $id;
continue;
};
}
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (in_array($newId, $idArray)) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
}
// Generate inner html as a string
@@ -170,49 +189,14 @@ 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-' . mb_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
*/
protected function pageToPlainText(Page $page) : string
public function pageToPlainText(Page $page)
{
$html = $this->renderPage($page, true);
$html = $this->renderPage($page);
return strip_tags($html);
}
@@ -422,29 +406,25 @@ class PageRepo extends EntityRepo
return [];
}
$tree = collect($headers)->map(function($header) {
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
if (mb_strlen($text) > 30) {
$text = mb_substr($text, 0, 27) . '...';
}
return [
$tree = collect([]);
foreach ($headers as $header) {
$text = $header->nodeValue;
$tree->push([
'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)),
'link' => '#' . $header->getAttribute('id'),
'text' => $text,
];
})->filter(function($header) {
return mb_strlen($header['text']) > 0;
});
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
]);
}
// Normalise headers if only smaller headers have been used
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
}
return $tree->toArray();
}
@@ -525,4 +505,4 @@ class PageRepo extends EntityRepo
return $this->publishPageDraft($copyPage, $pageData);
}
}
}

View File

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

View File

@@ -2,6 +2,4 @@
use Exception;
class HttpFetchException extends Exception
{
}
class HttpFetchException extends Exception {}

View File

@@ -1,5 +1,3 @@
<?php namespace BookStack\Exceptions;
class UserUpdateException extends NotifyException
{
}
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|min:2|max:255',
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6',
]);
@@ -142,7 +142,7 @@ class RegisterController extends Controller
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
@@ -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,10 +3,8 @@
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;
@@ -17,29 +15,18 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
* @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
* @param \BookStack\Auth\UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
*/
public function __construct(
EntityRepo $entityRepo,
UserRepo $userRepo,
ExportService $exportService,
EntityContextManager $entityContextManager,
ImageRepo $imageRepo
) {
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@@ -49,117 +36,67 @@ class BookController extends Controller
*/
public function index()
{
$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);
$books = $this->entityRepo->getAllPaginated('book', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$this->entityContextManager->clearShelfContext();
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$this->setPageTitle(trans('entities.books'));
return view('books.index', [
return view('books/index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
'booksViewType' => $booksViewType
]);
}
/**
* Show the form for creating a new book.
* @param string $shelfSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function create(string $shelfSlug = null)
public function create()
{
$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', [
'bookshelf' => $bookshelf
]);
return view('books/create');
}
/**
* Store a newly created book in storage.
*
* @param Request $request
* @param string $shelfSlug
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request, string $shelfSlug = null)
public function store(Request $request)
{
$this->checkPermission('book-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
'description' => 'string|max:1000'
]);
$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, Request $request)
public function show($slug)
{
$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, 1)
'activity' => Activity::entityActivity($book, 20, 0)
]);
}
@@ -173,32 +110,25 @@ 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, string $slug)
public function update(Request $request, $slug)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
'description' => 'string|max:1000'
]);
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@@ -212,24 +142,22 @@ 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, 'bookChildren' => $bookChildren]);
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
/**
@@ -242,7 +170,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]);
}
/**
@@ -326,12 +254,7 @@ 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');
}
@@ -340,12 +263,12 @@ class BookController extends Controller
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showPermissions($bookSlug)
public function showRestrict($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books.permissions', [
return view('books/restrictions', [
'book' => $book,
'roles' => $roles
]);
@@ -354,12 +277,11 @@ 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 permissions($bookSlug, Request $request)
public function restrict($bookSlug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
@@ -403,29 +325,4 @@ 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,9 +3,8 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Uploads\ImageRepo;
use BookStack\Entities\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -15,22 +14,19 @@ class BookshelfController extends Controller
protected $entityRepo;
protected $userRepo;
protected $entityContextManager;
protected $imageRepo;
protected $exportService;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
* @param \BookStack\Entities\ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
$this->exportService = $exportService;
parent::__construct();
}
@@ -40,35 +36,19 @@ class BookshelfController extends Controller
*/
public function index()
{
$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);
}
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
return view('shelves.index', [
return view('shelves/index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
'shelvesViewType' => $shelvesViewType
]);
}
@@ -81,14 +61,13 @@ 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)
{
@@ -96,14 +75,13 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->shelfUpdateActions($shelf, $request);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
Activity::add($bookshelf, 'bookshelf_create');
Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
return redirect($bookshelf->getUrl());
}
@@ -115,20 +93,17 @@ class BookshelfController extends Controller
*/
public function show(string $slug)
{
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('book-view', $shelf);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($shelf);
Views::add($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->setPageTitle($shelf->getShortName());
return view('shelves.show', [
'shelf' => $shelf,
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
'books' => $books,
'activity' => Activity::entityActivity($shelf, 20, 1)
'activity' => Activity::entityActivity($bookshelf, 20, 0)
]);
}
@@ -140,19 +115,19 @@ class BookshelfController extends Controller
*/
public function edit(string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id);
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
return view('shelves.edit', [
'shelf' => $shelf,
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
'shelf' => $bookshelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
@@ -161,11 +136,10 @@ 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)
{
@@ -174,12 +148,10 @@ 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->shelfUpdateActions($shelf, $request);
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
@@ -194,11 +166,11 @@ class BookshelfController extends Controller
*/
public function showDelete(string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
return view('shelves.delete', ['shelf' => $shelf]);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
}
/**
@@ -210,52 +182,46 @@ class BookshelfController extends Controller
*/
public function destroy(string $slug)
{
$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);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
return redirect('/shelves');
}
/**
* Show the permissions view.
* @param string $slug
* Show the Restrictions view.
* @param $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showPermissions(string $slug)
public function showRestrict(string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.permissions', [
'shelf' => $shelf,
return view('shelves.restrictions', [
'shelf' => $bookshelf,
'roles' => $roles
]);
}
/**
* Set the permissions for this bookshelf.
* @param string $slug
* Set the restrictions for this bookshelf.
* @param $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function permissions(string $slug, Request $request)
public function restrict(string $slug, Request $request)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($shelf->getUrl());
return redirect($bookshelf->getUrl());
}
/**
@@ -266,38 +232,11 @@ class BookshelfController extends Controller
*/
public function copyPermissions(string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
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();
}
return redirect($bookshelf->getUrl());
}
}

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,8 +161,7 @@ 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);
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters.move', [
return view('chapters/move', [
'chapter' => $chapter,
'book' => $chapter->book
]);
@@ -180,7 +179,6 @@ 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 === '') {
@@ -214,14 +212,13 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showPermissions($bookSlug, $chapterSlug)
public function showRestrict($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters.permissions', [
return view('chapters/restrictions', [
'chapter' => $chapter,
'roles' => $roles
]);
@@ -233,10 +230,8 @@ class ChapterController extends Controller
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function permissions($bookSlug, $chapterSlug, Request $request)
public function restrict($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,20 +123,6 @@ 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,6 +19,7 @@ class HomeController extends Controller
parent::__construct();
}
/**
* Display the homepage.
* @return Response
@@ -44,39 +45,17 @@ 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, $commonData['sort'], $commonData['order']);
foreach ($shelves as $shelf) {
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
}
$data = array_merge($commonData, ['shelves' => $shelves]);
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
return view('common.home-book', $data);
}
@@ -126,7 +105,7 @@ class HomeController extends Controller
*/
public function customHeadContent()
{
return view('partials.custom-head-content');
return view('partials/custom-head-content');
}
/**
@@ -141,7 +120,7 @@ class HomeController extends Controller
$allowRobots = $sitePublic;
}
return response()
->view('common.robots', ['allowRobots' => $allowRobots])
->view('common/robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
@@ -150,6 +129,6 @@ class HomeController extends Controller
*/
public function getNotFound()
{
return response()->view('errors.404', [], 404);
return response()->view('errors/404', [], 404);
}
}

View File

@@ -0,0 +1,247 @@
<?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

@@ -1,88 +0,0 @@
<?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

@@ -1,64 +0,0 @@
<?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

@@ -1,115 +0,0 @@
<?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,13 +377,12 @@ 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, 'current' => $page]);
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
@@ -404,10 +403,9 @@ 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
]);
}
@@ -434,7 +432,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,
@@ -484,12 +482,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]);
}
/**
@@ -497,13 +495,15 @@ class PageController extends Controller
* https://github.com/barryvdh/laravel-dompdf
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $pageSlug)
public function exportPdf($bookSlug, $pageSlug, Request $request)
{
$isTesting = $request->query('isTesting');
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$page->html = $this->pageRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
$pdfContent = $this->exportService->pageToPdf($page, !empty($isTesting));
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
@@ -534,20 +534,49 @@ 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
@@ -559,8 +588,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
return view('pages/move', [
'book' => $page->book,
'page' => $page
]);
@@ -578,7 +606,6 @@ 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 === '') {
@@ -616,9 +643,9 @@ class PageController extends Controller
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-view', $page);
$this->checkOwnablePermission('page-update', $page);
session()->flashInput(['name' => $page->name]);
return view('pages.copy', [
return view('pages/copy', [
'book' => $page->book,
'page' => $page
]);
@@ -635,7 +662,7 @@ class PageController extends Controller
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-view', $page);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -663,24 +690,6 @@ 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
@@ -688,9 +697,8 @@ class PageController extends Controller
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
* @throws \Throwable
*/
public function permissions($bookSlug, $pageSlug, Request $request)
public function restrict($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,45 +1,34 @@
<?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 EntityRepo $entityRepo
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
* @param EntityContextManager $entityContextManager
*/
public function __construct(
EntityRepo $entityRepo,
ViewService $viewService,
SearchService $searchService,
EntityContextManager $entityContextManager
) {
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
/**
* Searches all entities.
* @param Request $request
* @return View
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function search(Request $request)
@@ -52,7 +41,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,
@@ -66,28 +55,28 @@ class SearchController extends Controller
* Searches all entities within a book.
* @param Request $request
* @param integer $bookId
* @return View
* @return \Illuminate\View\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 View
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials.entity-list', ['entities' => $results]);
return view('partials/entity-list', ['entities' => $results]);
}
/**
@@ -98,64 +87,21 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['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) .'}';
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
$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);
}
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']);
return view('search/entity-ajax-list', ['entities' => $entities]);
}
}

View File

@@ -1,7 +1,5 @@
<?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;
@@ -9,19 +7,6 @@ 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
@@ -34,10 +19,7 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings.index', [
'version' => $version,
'guestUser' => User::getDefault()
]);
return view('settings/index', ['version' => $version]);
}
/**
@@ -49,9 +31,6 @@ 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) {
@@ -59,21 +38,7 @@ class SettingController extends Controller
continue;
}
$key = str_replace('setting-', '', trim($name));
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');
Setting::put($key, $value);
}
session()->flash('success', trans('settings.settings_save_success'));
@@ -92,7 +57,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

@@ -4,7 +4,6 @@ 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;
@@ -13,19 +12,16 @@ class UserController extends Controller
protected $user;
protected $userRepo;
protected $imageRepo;
/**
* UserController constructor.
* @param User $user
* @param User $user
* @param UserRepo $userRepo
* @param ImageRepo $imageRepo
*/
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
public function __construct(User $user, UserRepo $userRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@@ -45,7 +41,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]);
}
/**
@@ -57,7 +53,7 @@ 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]);
}
/**
@@ -111,7 +107,9 @@ class UserController extends Controller
*/
public function edit($id, SocialAuthService $socialAuthService)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->user->findOrFail($id);
@@ -120,29 +118,29 @@ 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->checkPermissionOrCurrentUser('users-manage', $id);
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $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',
'profile_image' => $this->imageRepo->getImageValidationRules(),
'setting' => 'array'
]);
$user = $this->userRepo->getById($id);
@@ -172,23 +170,10 @@ 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);
}
@@ -199,11 +184,13 @@ class UserController extends Controller
*/
public function delete($id)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $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]);
}
/**
@@ -215,7 +202,9 @@ class UserController extends Controller
public function destroy($id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->userRepo->getById($id);
@@ -243,12 +232,10 @@ 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,
@@ -264,7 +251,19 @@ class UserController extends Controller
*/
public function switchBookView($id, Request $request)
{
return $this->switchViewType($id, $request, 'books');
$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");
}
/**
@@ -275,97 +274,18 @@ class UserController extends Controller
*/
public function switchShelfView($id, Request $request)
{
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);
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->userRepo->getById($userId);
$key = $listName . '_view_type';
setting()->putUser($user, $key, $viewType);
$user = $this->userRepo->getById($id);
setting()->putUser($user, 'bookshelves_view_type', $viewType);
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");
return redirect()->back(302, [], "/settings/users/$id");
}
}

View File

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

View File

@@ -7,40 +7,8 @@ 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.
*
@@ -51,7 +19,6 @@ 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);
@@ -66,7 +33,6 @@ class Localization
app()->setLocale($locale);
Carbon::setLocale($locale);
$this->setSystemDateLocale($locale);
return $next($request);
}
@@ -87,18 +53,4 @@ 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,4 +31,5 @@ class MailNotification extends Notification implements ShouldQueue
'text' => 'vendor.notifications.email-plain'
]);
}
}
}

View File

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

View File

@@ -3,14 +3,11 @@
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;
@@ -25,14 +22,9 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
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;
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);
});
// Custom blade view directives
@@ -50,9 +42,6 @@ class AppServiceProvider extends ServiceProvider
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
]);
// View Composers
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
}
/**

View File

@@ -2,11 +2,20 @@
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\HttpFetcher;
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
{
@@ -28,19 +37,34 @@ class CustomFacadeProvider extends ServiceProvider
public function register()
{
$this->app->bind('activity', function () {
return $this->app->make(ActivityService::class);
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('views', function () {
return $this->app->make(ViewService::class);
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('setting', function () {
return $this->app->make(SettingService::class);
return new SettingService(
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
});
$this->app->bind('images', function () {
return $this->app->make(ImageService::class);
return new ImageService(
$this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class),
$this->app->make(HttpFetcher::class)
);
});
}
}

View File

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

View File

@@ -4,8 +4,10 @@ use Illuminate\Contracts\Cache\Repository as Cache;
/**
* Class SettingService
*
* The settings are a simple key-value database store.
* For non-authenticated users, user settings are stored via the session instead.
*
* @package BookStack\Services
*/
class SettingService
{
@@ -39,7 +41,6 @@ class SettingService
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
@@ -50,19 +51,6 @@ class SettingService
return $formatted;
}
/**
* Get a value from the session instead of the main store option.
* @param $key
* @param bool $default
* @return mixed
*/
protected function getFromSession($key, $default = false)
{
$value = session()->get($key, $default);
$formatted = $this->formatValue($value, $default);
return $formatted;
}
/**
* Get a user-specific setting from the database or cache.
* @param \BookStack\Auth\User $user
@@ -72,23 +60,9 @@ class SettingService
*/
public function getUser($user, $key, $default = false)
{
if ($user->isDefault()) {
return $this->getFromSession($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.
@@ -205,9 +179,6 @@ 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,5 +1,6 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
@@ -69,4 +70,5 @@ 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($uploadedFile);
$attachmentPath = $this->putFileInStorage($attachmentName, $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($uploadedFile);
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachment->name = $attachmentName;
$attachment->path = $attachmentPath;
@@ -174,18 +174,19 @@ 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(UploadedFile $uploadedFile)
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
$uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
$uploadFileName = $attachmentName;
while ($storage->exists($basePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}

View File

@@ -30,4 +30,5 @@ class HttpFetcher
return $data;
}
}
}

View File

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

View File

@@ -9,7 +9,6 @@ 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
@@ -58,29 +57,15 @@ 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,
string $type,
int $uploadedTo = 0,
int $resizeWidth = null,
int $resizeHeight = null,
bool $keepRatio = true
) {
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
{
$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);
}
@@ -137,7 +122,7 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
@@ -216,28 +201,8 @@ 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($imageData);
$thumb = $this->imageTool->make($storage->get($imagePath));
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
@@ -246,14 +211,20 @@ class ImageService extends UploadService
}
if ($keepRatio) {
$thumb->resize($width, $height, function ($constraint) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
} else {
$thumb->fit($width, $height);
}
return (string)$thumb->encode();
$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);
}
/**
@@ -335,7 +306,6 @@ class ImageService extends UploadService
$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;

View File

@@ -1,7 +1,5 @@
<?php
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Ownable;
/**
@@ -43,52 +41,30 @@ function user()
* Check if current user is a signed in user.
* @return bool
*/
function signedInUser() : bool
function signedInUser()
{
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 string $permission
* @param $permission
* @param Ownable $ownable
* @return mixed
*/
function userCan(string $permission, Ownable $ownable = null)
function userCan($permission, Ownable $ownable = null)
{
if ($ownable === null) {
return user() && user()->can($permission);
}
// Check permission on ownable item
$permissionService = app(PermissionService::class);
$permissionService = app(\BookStack\Auth\Permissions\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
@@ -123,11 +99,10 @@ function baseUrl($path, $forceAppDomain = false)
// Remove non-specified domain if forced and we have a domain
if ($isFullUrl && $forceAppDomain) {
if (!empty($base) && strpos($path, $base) === 0) {
$path = mb_substr($path, mb_strlen($base));
} else {
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
$path = trim(substr($path, strlen($base) - 1));
}
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
// Return normal url path if not specified in config
@@ -135,7 +110,7 @@ function baseUrl($path, $forceAppDomain = false)
return url($path);
}
return $base . '/' . ltrim($path, '/');
return $base . '/' . $path;
}
/**

View File

@@ -16,7 +16,7 @@
"laravel/framework": "~5.5.44",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4",
"laravel/socialite": "3.0.x-dev",
"laravel/socialite": "^3.0",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",

93
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": "0946a07729a7a1bfef9bac185a870afd",
"content-hash": "06219a5c2419ca23ec2924eb31f4ed16",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.86.2",
"version": "3.82.3",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
"s3",
"sdk"
],
"time": "2019-01-18T21:10:44+00:00"
"time": "2018-12-21T22:21:50+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
@@ -1457,16 +1457,16 @@
},
{
"name": "laravel/socialite",
"version": "3.0.x-dev",
"version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
"url": "https://api.github.com/repos/laravel/socialite/zipball/7194c0cd9fb2ce449669252b8ec316b85b7de481",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481",
"shasum": ""
},
"require": {
@@ -1516,7 +1516,7 @@
"laravel",
"oauth"
],
"time": "2018-12-21T14:06:32+00:00"
"time": "2018-10-18T03:39:04+00:00"
},
{
"name": "league/flysystem",
@@ -1891,16 +1891,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.36.2",
"version": "1.36.1",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"shasum": ""
},
"require": {
@@ -1945,7 +1945,7 @@
"datetime",
"time"
],
"time": "2018-12-28T10:07:33+00:00"
"time": "2018-11-22T18:23:02+00:00"
},
{
"name": "paragonie/random_compat",
@@ -2555,20 +2555,20 @@
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.4",
"version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"shasum": ""
},
"require": {
"laravel/socialite": "~3.0|~4.0",
"laravel/socialite": "~3.0",
"php": "^5.6 || ^7.0"
},
"require-dev": {
@@ -2585,7 +2585,8 @@
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/"
"SocialiteProviders\\Manager\\": "src/",
"SocialiteProviders\\Manager\\Test\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2596,14 +2597,10 @@
{
"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": "2019-01-16T07:58:54+00:00"
"time": "2017-11-20T08:42:57+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@@ -3667,16 +3664,16 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.5.2",
"version": "v2.5.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"shasum": ""
},
"require": {
@@ -3713,7 +3710,7 @@
"env",
"environment"
],
"time": "2018-10-30T17:29:25+00:00"
"time": "2018-07-29T20:33:41+00:00"
}
],
"packages-dev": [
@@ -4426,23 +4423,23 @@
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.8",
"version": "5.2.7",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4"
"reference": "8560d4314577199ba51bf2032f02cd1315587c23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.2.20",
"friendsofphp/php-cs-fixer": "^2.1",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
@@ -4488,7 +4485,7 @@
"json",
"schema"
],
"time": "2019-01-14T23:55:14+00:00"
"time": "2018-02-14T22:26:30+00:00"
},
{
"name": "laravel/browser-kit-testing",
@@ -6268,21 +6265,20 @@
},
{
"name": "webmozart/assert",
"version": "1.4.0",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
@@ -6315,14 +6311,12 @@
"check",
"validate"
],
"time": "2018-12-25T11:19:39+00:00"
"time": "2018-01-29T19:49:41+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"laravel/socialite": 20
},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -6332,8 +6326,7 @@
"ext-dom": "*",
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*",
"ext-curl": "*"
"ext-gd": "*"
},
"platform-dev": [],
"platform-overrides": {

View File

@@ -22,8 +22,7 @@ return [
// 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'),
'books' => env('APP_VIEWS_BOOKS', 'list')
],
// The number of revisions to keep in the database.
@@ -46,13 +45,13 @@ return [
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
// Application timezone for back-end date functions.
'timezone' => env('APP_TIMEZONE', 'UTC'),
'timezone' => 'UTC',
// Default locale to use
'locale' => env('APP_LANG', 'en'),
// 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'],
'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
// Application Fallback Locale
'fallback_locale' => 'en',

View File

@@ -8,39 +8,23 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
// REDIS
// Split out configuration into an array
// REDIS - Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServerKeys = ['host', 'port', 'database'];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [];
$cluster = count($redisServers) > 1;
if ($cluster) {
$redisConfig['clusters'] = ['default' => []];
}
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
$serverConfig = [];
$configIndex = 0;
foreach ($redisDefaults as $configKey => $configDefault) {
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
$configIndex++;
}
if ($cluster) {
$redisConfig['clusters']['default'][] = $serverConfig;
} else {
$redisConfig['default'] = $serverConfig;
}
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
}
}
// MYSQL
// Split out port from host if set
// 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);

View File

@@ -49,8 +49,6 @@ 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

@@ -141,7 +141,6 @@ 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'),

View File

@@ -14,6 +14,7 @@ return [
// Options: file, cookie, database, redis, memcached, array
'driver' => env('SESSION_DRIVER', 'file'),
// Session lifetime, in minutes
'lifetime' => env('SESSION_LIFETIME', 120),

2993
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": "^9.4.7",
"autoprefixer": "^8.6.5",
"babel-loader": "^8.0.4",
"css-loader": "^2.1.0",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.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": "^3.0.0",
"postcss-loader": "^2.1.6",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.3.0",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2"
},

67
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
header {
display: none; }
body {
font-size: 12px; }
.page-content {
margin: 0 auto; }
.flex-fill {
display: block; }
.flex.sidebar + .flex.content {
border-left: none; }
.print-hidden {
display: none; }
.print-full-width {
width: 100%;
float: none;
display: block; }
h2 {
font-size: 2em;
line-height: 1;
margin-top: 0.6em;
margin-bottom: 0.3em; }
.comments-container {
display: none; }

4487
public/dist/styles.css vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
!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);
!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)})}();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!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);
!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)})}();

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(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);
!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)})}();

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,7 +87,6 @@
<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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,8 @@ A platform for storing and organising information and documentation. General inf
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
* [Documentation](https://www.bookstackapp.com/docs)
* [Demo Instance](https://demo.bookstackapp.com)
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
* *Username: `admin@example.com`*
* *Password: `password`*
* [BookStack Blog](https://www.bookstackapp.com/blog)
## Project Definition
@@ -18,30 +19,7 @@ BookStack is an opinionated wiki system that provides a pleasant and simple out
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
## Road Map
Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
- **Design Revamp** *[(In Progress)](https://github.com/BookStackApp/BookStack/pull/1153)*
- *A more organised modern design to clean things up, make BookStack more efficient to use and increase mobile usability.*
- **Platform REST API**
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
- **Editor Alignment & Review**
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
- **Permission System Review**
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*
- **Installation & Deployment Process Revamp**
- *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
## Release Versioning & Process
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
In regards to development philosophy, BookStack has a relaxed, open & positive approach. Put simply, At the end of the day this is free software developed and maintained by people donating their own free time.
## Development & Testing
@@ -107,24 +85,15 @@ PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr
### Pull Requests
Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
## Website, Docs & Blog
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
## Security
Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
## License
@@ -148,6 +117,7 @@ These are the great open-source projects used to help build BookStack:
* [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
* [Dompdf](https://github.com/barryvdh/laravel-dompdf)

View File

@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 13.3h-5.7V19h-2.6v-5.7H5v-2.6h5.7V5h2.6v5.7H19z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 166 B

After

Width:  |  Height:  |  Size: 161 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.252 1.708H8.663a1.77 1.77 0 0 0-1.765 1.764v14.12c0 .97.794 1.764 1.765 1.764h10.59a1.77 1.77 0 0 0 1.764-1.765V3.472a1.77 1.77 0 0 0-1.765-1.764zM8.663 3.472h4.412v7.06L10.87 9.208l-2.206 1.324z"/><path d="M30.61 3.203h24v24h-24z" fill="none"/><path d="M2.966 6.61v14c0 1.1.9 2 2 2h14v-2h-14v-14z"/></svg>

Before

Width:  |  Height:  |  Size: 416 B

View File

@@ -1 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 375 B

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