Merge branch 'master' into release

This commit is contained in:
Dan Brown 2019-05-06 18:57:58 +01:00
commit e9914eb301
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
276 changed files with 8228 additions and 8356 deletions

View File

@ -32,6 +32,11 @@ APP_LANG=en
# APP_LANG will be used if such a header is not provided. # APP_LANG will be used if such a header is not provided.
APP_AUTO_LANG_PUBLIC=true 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 # Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used. # Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
DB_HOST=localhost DB_HOST=localhost

View File

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

View File

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

View File

@ -182,25 +182,14 @@ class LdapService
throw new LdapException(trans('errors.ldap_extension_not_installed')); throw new LdapException(trans('errors.ldap_extension_not_installed'));
} }
// Get port from server string and protocol if specified. // Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
$ldapServer = explode(':', $this->config['server']); // the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
$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']) { if ($this->config['tls_insecure']) {
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER); $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
} }
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort); $serverDetails = $this->parseServerString($this->config['server']);
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
if ($ldapConnection === false) { if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect')); throw new LdapException(trans('errors.ldap_cannot_connect'));
@ -215,6 +204,27 @@ class LdapService
return $this->ldapConnection; 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. * Build a filter string by injecting common variables.
* @param string $filterString * @param string $filterString
@ -319,10 +329,10 @@ class LdapService
$count = 0; $count = 0;
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { 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); $dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1);
if (!in_array($dnComponents[0], $ldapGroups)) { if (!in_array($dnComponents[0], $ldapGroups)) {
$ldapGroups[] = $dnComponents[0]; $ldapGroups[] = $dnComponents[0];

View File

@ -577,7 +577,7 @@ class PermissionService
$query2->where('has_permission_own', '=', 1) $query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId); ->where('created_by', '=', $userId);
}); });
}) ; });
if (!is_null($entityClass)) { if (!is_null($entityClass)) {
$entityInstance = app()->make($entityClass); $entityInstance = app()->make($entityClass);
@ -704,7 +704,7 @@ class PermissionService
* @param string $entityIdColumn * @param string $entityIdColumn
* @param string $entityTypeColumn * @param string $entityTypeColumn
* @param string $action * @param string $action
* @return mixed * @return QueryBuilder
*/ */
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view') public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{ {
@ -732,18 +732,21 @@ class PermissionService
} }
/** /**
* Filters pages that are a direct relation to another item. * Add conditions to a query to filter the selection to related entities
* where permissions are granted.
* @param $entityType
* @param $query * @param $query
* @param $tableName * @param $tableName
* @param $entityIdColumn * @param $entityIdColumn
* @return mixed * @return mixed
*/ */
public function filterRelatedPages($query, $tableName, $entityIdColumn) public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
{ {
$this->currentAction = 'view'; $this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$pageMorphClass = $this->entityProvider->page->getMorphClass(); $pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) { $q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) { $query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
@ -761,7 +764,9 @@ class PermissionService
}); });
})->orWhere($tableDetails['entityIdColumn'], '=', 0); })->orWhere($tableDetails['entityIdColumn'], '=', 0);
}); });
$this->clean(); $this->clean();
return $q; return $q;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,6 +102,11 @@ class Entity extends Ownable
return $this->morphMany(View::class, 'viewable'); 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. * Get the Tag models that have been user assigned to this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany * @return \Illuminate\Database\Eloquent\Relations\MorphMany
@ -218,6 +223,20 @@ class Entity extends Ownable
return $this->{$this->textField}; 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 a generalised, common raw query that can be 'unioned' across entities.
* @return string * @return string

View File

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

View File

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

View File

@ -102,17 +102,6 @@ class Page extends Entity
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent); 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. * Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent * @param bool $withContent

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -123,6 +123,20 @@ abstract class Controller extends BaseController
return true; 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. * Send back a json error message.
* @param string $messageText * @param string $messageText

View File

@ -19,7 +19,6 @@ class HomeController extends Controller
parent::__construct(); parent::__construct();
} }
/** /**
* Display the homepage. * Display the homepage.
* @return Response * @return Response
@ -45,17 +44,36 @@ class HomeController extends Controller
'draftPages' => $draftPages, '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') { if ($homepageOption === 'bookshelves') {
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); $data = array_merge($commonData, ['shelves' => $shelves]);
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
return view('common.home-shelves', $data); return view('common.home-shelves', $data);
} }
if ($homepageOption === 'books') { if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18); $books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); $data = array_merge($commonData, ['books' => $books]);
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
return view('common.home-book', $data); return view('common.home-book', $data);
} }
@ -105,7 +123,7 @@ class HomeController extends Controller
*/ */
public function customHeadContent() public function customHeadContent()
{ {
return view('partials/custom-head-content'); return view('partials.custom-head-content');
} }
/** /**
@ -120,7 +138,7 @@ class HomeController extends Controller
$allowRobots = $sitePublic; $allowRobots = $sitePublic;
} }
return response() return response()
->view('common/robots', ['allowRobots' => $allowRobots]) ->view('common.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain'); ->header('Content-Type', 'text/plain');
} }
@ -129,6 +147,6 @@ class HomeController extends Controller
*/ */
public function getNotFound() public function getNotFound()
{ {
return response()->view('errors/404', [], 404); return response()->view('errors.404', [], 404);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@ class PageController extends Controller
// Otherwise show the edit view if they're a guest // Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new')); $this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]); return view('pages.guest-create', ['parent' => $parent]);
} }
/** /**
@ -110,7 +110,7 @@ class PageController extends Controller
$this->setPageTitle(trans('entities.pages_edit_draft')); $this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn; $draftsEnabled = $this->signedIn;
return view('pages/edit', [ return view('pages.edit', [
'page' => $draft, 'page' => $draft,
'book' => $draft->book, 'book' => $draft->book,
'isDraft' => true, 'isDraft' => true,
@ -184,7 +184,7 @@ class PageController extends Controller
Views::add($page); Views::add($page);
$this->setPageTitle($page->getShortName()); $this->setPageTitle($page->getShortName());
return view('pages/show', [ return view('pages.show', [
'page' => $page,'book' => $page->book, 'page' => $page,'book' => $page->book,
'current' => $page, 'current' => $page,
'sidebarTree' => $sidebarTree, 'sidebarTree' => $sidebarTree,
@ -239,7 +239,7 @@ class PageController extends Controller
} }
$draftsEnabled = $this->signedIn; $draftsEnabled = $this->signedIn;
return view('pages/edit', [ return view('pages.edit', [
'page' => $page, 'page' => $page,
'book' => $page->book, 'book' => $page->book,
'current' => $page, 'current' => $page,
@ -317,7 +317,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]); return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
} }
@ -333,7 +333,7 @@ class PageController extends Controller
$page = $this->pageRepo->getById('page', $pageId, true); $page = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]); return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
} }
/** /**
@ -377,12 +377,13 @@ class PageController extends Controller
* @param string $bookSlug * @param string $bookSlug
* @param string $pageSlug * @param string $pageSlug
* @return \Illuminate\View\View * @return \Illuminate\View\View
* @throws NotFoundException
*/ */
public function showRevisions($bookSlug, $pageSlug) public function showRevisions($bookSlug, $pageSlug)
{ {
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); return view('pages.revisions', ['page' => $page, 'current' => $page]);
} }
/** /**
@ -403,9 +404,10 @@ class PageController extends Controller
$page->fill($revision->toArray()); $page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()])); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [ return view('pages.revision', [
'page' => $page, 'page' => $page,
'book' => $page->book, 'book' => $page->book,
'diff' => null,
'revision' => $revision 'revision' => $revision
]); ]);
} }
@ -432,7 +434,7 @@ class PageController extends Controller
$page->fill($revision->toArray()); $page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()])); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [ return view('pages.revision', [
'page' => $page, 'page' => $page,
'book' => $page->book, 'book' => $page->book,
'diff' => $diff, 'diff' => $diff,
@ -482,12 +484,12 @@ class PageController extends Controller
// Check if its the latest revision, cannot delete latest revision. // Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) { if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest')); 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(); $revision->delete();
session()->flash('success', trans('entities.revision_delete_success')); session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
} }
/** /**
@ -532,49 +534,20 @@ class PageController extends Controller
return $this->downloadResponse($pageText, $pageSlug . '.txt'); 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 * Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function showRecentlyUpdated() public function showRecentlyUpdated()
{ {
// TODO - Still exist?
$pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated')); $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [ return view('pages.detailed-listing', [
'title' => trans('entities.recently_updated_pages'), 'title' => trans('entities.recently_updated_pages'),
'pages' => $pages 'pages' => $pages
]); ]);
} }
/**
* 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. * Show the view to choose a new parent to move a page into.
* @param string $bookSlug * @param string $bookSlug
@ -587,7 +560,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
return view('pages/move', [ return view('pages.move', [
'book' => $page->book, 'book' => $page->book,
'page' => $page 'page' => $page
]); ]);
@ -645,7 +618,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]); session()->flashInput(['name' => $page->name]);
return view('pages/copy', [ return view('pages.copy', [
'book' => $page->book, 'book' => $page->book,
'page' => $page 'page' => $page
]); ]);
@ -690,6 +663,24 @@ class PageController extends Controller
return redirect($pageCopy->getUrl()); 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. * Set the permissions for this page.
* @param string $bookSlug * @param string $bookSlug
@ -697,8 +688,9 @@ class PageController extends Controller
* @param Request $request * @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException * @throws NotFoundException
* @throws \Throwable
*/ */
public function restrict($bookSlug, $pageSlug, Request $request) public function permissions($bookSlug, $pageSlug, Request $request)
{ {
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page); $this->checkOwnablePermission('restrictions-manage', $page);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,14 @@
use Blade; use Blade;
use BookStack\Entities\Book; use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf; use BookStack\Entities\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Chapter; use BookStack\Entities\Chapter;
use BookStack\Entities\Page; use BookStack\Entities\Page;
use BookStack\Settings\Setting; use BookStack\Settings\Setting;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Schema; use Schema;
use Validator; use Validator;
@ -33,7 +35,6 @@ class AppServiceProvider extends ServiceProvider
return substr_count($uploadName, '.') < 2; return substr_count($uploadName, '.') < 2;
}); });
// Custom blade view directives // Custom blade view directives
Blade::directive('icon', function ($expression) { Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>"; return "<?php echo icon($expression); ?>";
@ -49,6 +50,9 @@ class AppServiceProvider extends ServiceProvider
'BookStack\\Chapter' => Chapter::class, 'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class, 'BookStack\\Page' => Page::class,
]); ]);
// View Composers
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
} }
/** /**

View File

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

@ -61,9 +61,23 @@ class SettingService
*/ */
public function getUser($user, $key, $default = false) public function getUser($user, $key, $default = false)
{ {
if ($user->isDefault()) {
return session()->get($key, $default);
}
return $this->get($this->userKey($user->id, $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. * Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database. * Looks at the system defaults if not cached or in database.
@ -180,6 +194,9 @@ class SettingService
*/ */
public function putUser($user, $key, $value) public function putUser($user, $key, $value)
{ {
if ($user->isDefault()) {
return session()->put($key, $value);
}
return $this->put($this->userKey($user->id, $key), $value); return $this->put($this->userKey($user->id, $key), $value);
} }

View File

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

View File

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

View File

@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use phpDocumentor\Reflection\Types\Integer;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService extends UploadService class ImageService extends UploadService
@ -59,13 +60,27 @@ class ImageService extends UploadService
* @param UploadedFile $uploadedFile * @param UploadedFile $uploadedFile
* @param string $type * @param string $type
* @param int $uploadedTo * @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return mixed * @return mixed
* @throws ImageUploadException * @throws ImageUploadException
*/ */
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0) public function saveNewFromUpload(
{ UploadedFile $uploadedFile,
string $type,
int $uploadedTo = 0,
int $resizeWidth = null,
int $resizeHeight = null,
bool $keepRatio = true
) {
$imageName = $uploadedFile->getClientOriginalName(); $imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath()); $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); return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
} }
@ -122,7 +137,7 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images'); $secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName); $imageName = str_replace(' ', '-', $imageName);
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) { while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName; $imageName = str_random(3) . $imageName;
@ -201,8 +216,28 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath); 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 { try {
$thumb = $this->imageTool->make($storage->get($imagePath)); $thumb = $this->imageTool->make($imageData);
} catch (Exception $e) { } catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs')); throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
@ -211,20 +246,14 @@ class ImageService extends UploadService
} }
if ($keepRatio) { if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) { $thumb->resize($width, $height, function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
$constraint->upsize(); $constraint->upsize();
}); });
} else { } else {
$thumb->fit($width, $height); $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);
} }
/** /**
@ -306,6 +335,7 @@ class ImageService extends UploadService
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName); $image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
$image->created_by = $user->id; $image->created_by = $user->id;
$image->updated_by = $user->id; $image->updated_by = $user->id;
$image->uploaded_to = $user->id;
$image->save(); $image->save();
return $image; return $image;

View File

@ -43,11 +43,20 @@ function user()
* Check if current user is a signed in user. * Check if current user is a signed in user.
* @return bool * @return bool
*/ */
function signedInUser() function signedInUser() : bool
{ {
return auth()->user() && !auth()->user()->isDefault(); 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. * Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against * If an ownable element is passed in the jointPermissions are checked against

View File

@ -46,7 +46,7 @@ return [
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''), 'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
// Application timezone for back-end date functions. // Application timezone for back-end date functions.
'timezone' => 'UTC', 'timezone' => env('APP_TIMEZONE', 'UTC'),
// Default locale to use // Default locale to use
'locale' => env('APP_LANG', 'en'), 'locale' => env('APP_LANG', 'en'),

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -117,6 +117,15 @@ If you are looking to alter CSS or JavaScript content please edit the source fil
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. 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).
## License ## License
The BookStack source is provided under the MIT License. The BookStack source is provided under the MIT License.

View File

@ -1,4 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <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>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 161 B

After

Width:  |  Height:  |  Size: 166 B

View File

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

After

Width:  |  Height:  |  Size: 416 B

View File

@ -1,2 +1 @@
<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: 375 B

After

Width:  |  Height:  |  Size: 373 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.86 4.118l-9.733 9.609-3.951-3.995-2.98 2.966 6.93 7.184L21.805 7.217z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 161 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.034l6.57-6.554h-4.927V2.966h-3.286V14.48H5.43z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 168 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2.966L5.43 9.52h4.927v11.514h3.286V9.52h4.927z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 165 B

View File

@ -0,0 +1,58 @@
class BreadcrumbListing {
constructor(elem) {
this.elem = elem;
this.searchInput = elem.querySelector('input');
this.loadingElem = elem.querySelector('.loading-container');
this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
this.toggleElem = elem.querySelector('[dropdown-toggle]');
// this.loadingElem.style.display = 'none';
const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
this.entityType = entityDescriptor[0];
this.entityId = Number(entityDescriptor[1]);
this.toggleElem.addEventListener('click', this.onShow.bind(this));
this.searchInput.addEventListener('input', this.onSearch.bind(this));
}
onShow() {
this.loadEntityView();
}
onSearch() {
const input = this.searchInput.value.toLowerCase().trim();
const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
for (let listItem of listItems) {
const match = !input || listItem.textContent.toLowerCase().includes(input);
listItem.style.display = match ? 'flex' : 'none';
}
}
loadEntityView() {
this.toggleLoading(true);
const params = {
'entity_id': this.entityId,
'entity_type': this.entityType,
};
window.$http.get('/search/entity/siblings', {params}).then(resp => {
this.entityListElem.innerHTML = resp.data;
}).catch(err => {
console.error(err);
}).then(() => {
this.toggleLoading(false);
this.onSearch();
});
}
toggleLoading(show = false) {
this.loadingElem.style.display = show ? 'block' : 'none';
}
}
export default BreadcrumbListing;

View File

@ -6,24 +6,60 @@ class DropDown {
constructor(elem) { constructor(elem) {
this.container = elem; this.container = elem;
this.menu = elem.querySelector('ul'); this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
this.moveMenu = elem.hasAttribute('dropdown-move-menu');
this.toggle = elem.querySelector('[dropdown-toggle]'); this.toggle = elem.querySelector('[dropdown-toggle]');
this.body = document.body;
this.setupListeners(); this.setupListeners();
} }
show() { show(event) {
this.hideAll();
this.menu.style.display = 'block'; this.menu.style.display = 'block';
this.menu.classList.add('anim', 'menuIn'); this.menu.classList.add('anim', 'menuIn');
this.container.addEventListener('mouseleave', this.hide.bind(this));
if (this.moveMenu) {
// Move to body to prevent being trapped within scrollable sections
this.rect = this.menu.getBoundingClientRect();
this.body.appendChild(this.menu);
this.menu.style.position = 'fixed';
this.menu.style.left = `${this.rect.left}px`;
this.menu.style.top = `${this.rect.top}px`;
this.menu.style.width = `${this.rect.width}px`;
}
// Set listener to hide on mouse leave or window click
this.menu.addEventListener('mouseleave', this.hide.bind(this));
window.addEventListener('click', event => {
if (!this.menu.contains(event.target)) {
this.hide();
}
});
// Focus on first input if existing // Focus on first input if existing
let input = this.menu.querySelector('input'); let input = this.menu.querySelector('input');
if (input !== null) input.focus(); if (input !== null) input.focus();
event.stopPropagation();
}
hideAll() {
for (let dropdown of window.components.dropdown) {
dropdown.hide();
}
} }
hide() { hide() {
this.menu.style.display = 'none'; this.menu.style.display = 'none';
this.menu.classList.remove('anim', 'menuIn'); this.menu.classList.remove('anim', 'menuIn');
if (this.moveMenu) {
this.menu.style.position = '';
this.menu.style.left = '';
this.menu.style.top = '';
this.menu.style.width = '';
this.container.appendChild(this.menu);
}
} }
setupListeners() { setupListeners() {

View File

@ -5,15 +5,17 @@ class EntitySelector {
this.elem = elem; this.elem = elem;
this.search = ''; this.search = '';
this.lastClick = 0; this.lastClick = 0;
this.selectedItemData = null;
let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter'; const entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
let entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view'; const entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`); this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
this.input = elem.querySelector('[entity-selector-input]'); this.input = elem.querySelector('[entity-selector-input]');
this.searchInput = elem.querySelector('[entity-selector-search]'); this.searchInput = elem.querySelector('[entity-selector-search]');
this.loading = elem.querySelector('[entity-selector-loading]'); this.loading = elem.querySelector('[entity-selector-loading]');
this.resultsContainer = elem.querySelector('[entity-selector-results]'); this.resultsContainer = elem.querySelector('[entity-selector-results]');
this.addButton = elem.querySelector('[entity-selector-add-button]');
this.elem.addEventListener('click', this.onClick.bind(this)); this.elem.addEventListener('click', this.onClick.bind(this));
@ -26,10 +28,20 @@ class EntitySelector {
this.searchEntities(this.searchInput.value); this.searchEntities(this.searchInput.value);
}, 200); }, 200);
}); });
this.searchInput.addEventListener('keydown', event => { this.searchInput.addEventListener('keydown', event => {
if (event.keyCode === 13) event.preventDefault(); if (event.keyCode === 13) event.preventDefault();
}); });
if (this.addButton) {
this.addButton.addEventListener('click', event => {
if (this.selectedItemData) {
this.confirmSelection(this.selectedItemData);
this.unselectAll();
}
});
}
this.showLoading(); this.showLoading();
this.initialLoad(); this.initialLoad();
} }
@ -53,7 +65,7 @@ class EntitySelector {
searchEntities(searchTerm) { searchEntities(searchTerm) {
this.input.value = ''; this.input.value = '';
let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`; let url = `${this.searchUrl}&term=${encodeURIComponent(searchTerm)}`;
window.$http.get(url).then(resp => { window.$http.get(url).then(resp => {
this.resultsContainer.innerHTML = resp.data; this.resultsContainer.innerHTML = resp.data;
this.hideLoading(); this.hideLoading();
@ -68,49 +80,54 @@ class EntitySelector {
} }
onClick(event) { onClick(event) {
let t = event.target; const listItem = event.target.closest('[data-entity-type]');
if (listItem) {
if (t.matches('.entity-list-item *')) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
let item = t.closest('[data-entity-type]'); this.selectItem(listItem);
this.selectItem(item);
} else if (t.matches('[data-entity-type]')) {
this.selectItem(t)
} }
} }
selectItem(item) { selectItem(item) {
let isDblClick = this.isDoubleClick(); const isDblClick = this.isDoubleClick();
let type = item.getAttribute('data-entity-type'); const type = item.getAttribute('data-entity-type');
let id = item.getAttribute('data-entity-id'); const id = item.getAttribute('data-entity-id');
let isSelected = !item.classList.contains('selected') || isDblClick; const isSelected = (!item.classList.contains('selected') || isDblClick);
this.unselectAll(); this.unselectAll();
this.input.value = isSelected ? `${type}:${id}` : ''; this.input.value = isSelected ? `${type}:${id}` : '';
if (!isSelected) window.$events.emit('entity-select-change', null); const link = item.getAttribute('href');
const name = item.querySelector('.entity-list-item-name').textContent;
const data = {id: Number(id), name: name, link: link};
if (isSelected) { if (isSelected) {
item.classList.add('selected'); item.classList.add('selected');
item.classList.add('primary-background'); this.selectedItemData = data;
} else {
window.$events.emit('entity-select-change', null)
} }
if (!isDblClick && !isSelected) return; if (!isDblClick && !isSelected) return;
let link = item.querySelector('.entity-list-item-link').getAttribute('href'); if (isDblClick) {
let name = item.querySelector('.entity-list-item-name').textContent; this.confirmSelection(data);
let data = {id: Number(id), name: name, link: link}; }
if (isSelected) {
window.$events.emit('entity-select-change', data)
}
}
if (isDblClick) window.$events.emit('entity-select-confirm', data); confirmSelection(data) {
if (isSelected) window.$events.emit('entity-select-change', data); window.$events.emit('entity-select-confirm', data);
} }
unselectAll() { unselectAll() {
let selected = this.elem.querySelectorAll('.selected'); let selected = this.elem.querySelectorAll('.selected');
for (let i = 0, len = selected.length; i < len; i++) { for (let selectedElem of selected) {
selected[i].classList.remove('selected'); selectedElem.classList.remove('selected', 'primary-background');
selected[i].classList.remove('primary-background');
} }
this.selectedItemData = null;
} }
} }

View File

@ -3,8 +3,13 @@ class ExpandToggle {
constructor(elem) { constructor(elem) {
this.elem = elem; this.elem = elem;
this.isOpen = false;
// Component state
this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
this.selector = elem.getAttribute('expand-toggle'); this.selector = elem.getAttribute('expand-toggle');
// Listener setup
elem.addEventListener('click', this.click.bind(this)); elem.addEventListener('click', this.click.bind(this));
} }
@ -53,11 +58,20 @@ class ExpandToggle {
click(event) { click(event) {
event.preventDefault(); event.preventDefault();
let matchingElems = document.querySelectorAll(this.selector);
for (let i = 0, len = matchingElems.length; i < len; i++) { const matchingElems = document.querySelectorAll(this.selector);
this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]); for (let match of matchingElems) {
this.isOpen ? this.close(match) : this.open(match);
} }
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
this.updateSystemAjax(this.isOpen);
}
updateSystemAjax(isOpen) {
window.$http.patch(this.updateEndpoint, {
expand: isOpen ? 'true' : 'false'
});
} }
} }

View File

@ -0,0 +1,31 @@
class HeaderMobileToggle {
constructor(elem) {
this.elem = elem;
this.toggleButton = elem.querySelector('.mobile-menu-toggle');
this.menu = elem.querySelector('.header-links');
this.open = false;
this.toggleButton.addEventListener('click', this.onToggle.bind(this));
this.onWindowClick = this.onWindowClick.bind(this);
}
onToggle(event) {
this.open = !this.open;
this.menu.classList.toggle('show', this.open);
if (this.open) {
window.addEventListener('click', this.onWindowClick)
} else {
window.removeEventListener('click', this.onWindowClick)
}
event.stopPropagation();
}
onWindowClick(event) {
this.onToggle(event);
}
}
module.exports = HeaderMobileToggle;

View File

@ -4,54 +4,50 @@ class ImagePicker {
constructor(elem) { constructor(elem) {
this.elem = elem; this.elem = elem;
this.imageElem = elem.querySelector('img'); this.imageElem = elem.querySelector('img');
this.input = elem.querySelector('input'); this.imageInput = elem.querySelector('input[type=file]');
this.resetInput = elem.querySelector('input[data-reset-input]');
this.removeInput = elem.querySelector('input[data-remove-input]');
this.isUsingIds = elem.getAttribute('data-current-id') !== ''; this.defaultImage = elem.getAttribute('data-default-image');
this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width');
this.isResizeCropping = elem.getAttribute('data-resize-crop') !== '';
let selectButton = elem.querySelector('button[data-action="show-image-manager"]'); const resetButton = elem.querySelector('button[data-action="reset-image"]');
selectButton.addEventListener('click', this.selectImage.bind(this));
let resetButton = elem.querySelector('button[data-action="reset-image"]');
resetButton.addEventListener('click', this.reset.bind(this)); resetButton.addEventListener('click', this.reset.bind(this));
let removeButton = elem.querySelector('button[data-action="remove-image"]'); const removeButton = elem.querySelector('button[data-action="remove-image"]');
if (removeButton) { if (removeButton) {
removeButton.addEventListener('click', this.removeImage.bind(this)); removeButton.addEventListener('click', this.removeImage.bind(this));
} }
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
} }
selectImage() { fileInputChange() {
window.ImageManager.show(image => { this.resetInput.setAttribute('disabled', 'disabled');
if (!this.isResizing) { if (this.removeInput) {
this.setImage(image); this.removeInput.setAttribute('disabled', 'disabled');
return;
} }
let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false'); for (let file of this.imageInput.files) {
this.imageElem.src = window.URL.createObjectURL(file);
window.$http.get(window.baseUrl(requestString)).then(resp => { }
image.url = resp.data.url; this.imageElem.classList.remove('none');
this.setImage(image);
});
});
} }
reset() { reset() {
this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')}); this.imageInput.value = '';
this.imageElem.src = this.defaultImage;
this.resetInput.removeAttribute('disabled');
if (this.removeInput) {
this.removeInput.setAttribute('disabled', 'disabled');
} }
setImage(image) {
this.imageElem.src = image.url;
this.input.value = this.isUsingIds ? image.id : image.url;
this.imageElem.classList.remove('none'); this.imageElem.classList.remove('none');
} }
removeImage() { removeImage() {
this.imageElem.src = this.elem.getAttribute('data-default-image'); this.imageInput.value = '';
this.imageElem.classList.add('none'); this.imageElem.classList.add('none');
this.input.value = 'none'; this.removeInput.removeAttribute('disabled');
this.resetInput.setAttribute('disabled', 'disabled');
} }
} }

View File

@ -18,7 +18,11 @@ import toggleSwitch from "./toggle-switch";
import pageDisplay from "./page-display"; import pageDisplay from "./page-display";
import shelfSort from "./shelf-sort"; import shelfSort from "./shelf-sort";
import homepageControl from "./homepage-control"; import homepageControl from "./homepage-control";
import headerMobileToggle from "./header-mobile-toggle";
import listSortControl from "./list-sort-control";
import triLayout from "./tri-layout";
import breadcrumbListing from "./breadcrumb-listing";
import permissionsTable from "./permissions-table";
const componentMapping = { const componentMapping = {
'dropdown': dropdown, 'dropdown': dropdown,
@ -41,6 +45,11 @@ const componentMapping = {
'page-display': pageDisplay, 'page-display': pageDisplay,
'shelf-sort': shelfSort, 'shelf-sort': shelfSort,
'homepage-control': homepageControl, 'homepage-control': homepageControl,
'header-mobile-toggle': headerMobileToggle,
'list-sort-control': listSortControl,
'tri-layout': triLayout,
'breadcrumb-listing': breadcrumbListing,
'permissions-table': permissionsTable,
}; };
window.components = {}; window.components = {};

View File

@ -0,0 +1,45 @@
/**
* ListSortControl
* Manages the logic for the control which provides list sorting options.
*/
class ListSortControl {
constructor(elem) {
this.elem = elem;
this.menu = elem.querySelector('ul');
this.sortInput = elem.querySelector('[name="sort"]');
this.orderInput = elem.querySelector('[name="order"]');
this.form = elem.querySelector('form');
this.menu.addEventListener('click', event => {
if (event.target.closest('[data-sort-value]') !== null) {
this.sortOptionClick(event);
}
});
this.elem.addEventListener('click', event => {
if (event.target.closest('[data-sort-dir]') !== null) {
this.sortDirectionClick(event);
}
});
}
sortOptionClick(event) {
const sortOption = event.target.closest('[data-sort-value]');
this.sortInput.value = sortOption.getAttribute('data-sort-value');
event.preventDefault();
this.form.submit();
}
sortDirectionClick(event) {
const currentDir = this.orderInput.value;
const newDir = (currentDir === 'asc') ? 'desc' : 'asc';
this.orderInput.value = newDir;
event.preventDefault();
this.form.submit();
}
}
export default ListSortControl;

View File

@ -64,13 +64,26 @@ class MarkdownEditor {
let action = button.getAttribute('data-action'); let action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage(); if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector(); if (action === 'insertLink') this.actionShowLinkSelector();
if (action === 'insertDrawing' && event.ctrlKey) { if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
this.actionShowImageManager(); this.actionShowImageManager();
return; return;
} }
if (action === 'insertDrawing') this.actionStartDrawing(); if (action === 'insertDrawing') this.actionStartDrawing();
}); });
// Mobile section toggling
this.elem.addEventListener('click', event => {
const toolbarLabel = event.target.closest('.editor-toolbar-label');
if (!toolbarLabel) return;
const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');
for (let activeElem of currentActiveSections) {
activeElem.classList.remove('active');
}
toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
});
window.$events.listen('editor-markdown-update', value => { window.$events.listen('editor-markdown-update', value => {
this.cm.setValue(value); this.cm.setValue(value);
this.updateAndRender(); this.updateAndRender();
@ -381,9 +394,7 @@ class MarkdownEditor {
const drawingId = imgContainer.getAttribute('drawio-diagram'); const drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(() => { DrawIO.show(() => {
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { return DrawIO.load(drawingId);
return `data:image/png;base64,${resp.data.content}`;
});
}, (pngData) => { }, (pngData) => {
let data = { let data = {

View File

@ -6,7 +6,7 @@ class Overlay {
elem.addEventListener('click', event => { elem.addEventListener('click', event => {
if (event.target === elem) return this.hide(); if (event.target === elem) return this.hide();
}); });
let closeButtons = elem.querySelectorAll('.overlay-close'); let closeButtons = elem.querySelectorAll('.popup-header-close');
for (let i=0; i < closeButtons.length; i++) { for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this)); closeButtons[i].addEventListener('click', this.hide.bind(this));
} }

View File

@ -54,7 +54,7 @@ class PageComments {
commentElem.querySelector('[comment-edit-container]').style.display = 'block'; commentElem.querySelector('[comment-edit-container]').style.display = 'block';
let textArea = commentElem.querySelector('[comment-edit-container] textarea'); let textArea = commentElem.querySelector('[comment-edit-container] textarea');
let lineCount = textArea.value.split('\n').length; let lineCount = textArea.value.split('\n').length;
textArea.style.height = (lineCount * 20) + 'px'; textArea.style.height = ((lineCount * 20) + 40) + 'px';
this.editingComment = commentElem; this.editingComment = commentElem;
} }
@ -88,6 +88,7 @@ class PageComments {
commentElem.parentNode.removeChild(commentElem); commentElem.parentNode.removeChild(commentElem);
window.$events.emit('success', window.trans('entities.comment_deleted_success')); window.$events.emit('success', window.trans('entities.comment_deleted_success'));
this.updateCount(); this.updateCount();
this.hideForm();
}); });
} }
@ -129,7 +130,7 @@ class PageComments {
showForm() { showForm() {
this.formContainer.style.display = 'block'; this.formContainer.style.display = 'block';
this.formContainer.parentNode.style.display = 'block'; this.formContainer.parentNode.style.display = 'block';
this.elem.querySelector('[comment-add-button]').style.display = 'none'; this.elem.querySelector('[comment-add-button-container]').style.display = 'none';
this.formInput.focus(); this.formInput.focus();
window.scrollToElement(this.formInput); window.scrollToElement(this.formInput);
} }
@ -137,7 +138,18 @@ class PageComments {
hideForm() { hideForm() {
this.formContainer.style.display = 'none'; this.formContainer.style.display = 'none';
this.formContainer.parentNode.style.display = 'none'; this.formContainer.parentNode.style.display = 'none';
this.elem.querySelector('[comment-add-button]').style.display = 'block'; const addButtonContainer = this.elem.querySelector('[comment-add-button-container]');
if (this.getCommentCount() > 0) {
this.elem.appendChild(addButtonContainer)
} else {
const countBar = this.elem.querySelector('[comment-count-bar]');
countBar.appendChild(addButtonContainer);
}
addButtonContainer.style.display = 'block';
}
getCommentCount() {
return this.elem.querySelectorAll('.comment-box[comment]').length;
} }
setReply(commentElem) { setReply(commentElem) {

View File

@ -184,9 +184,9 @@ class PageDisplay {
setupNavHighlighting() { setupNavHighlighting() {
// Check if support is present for IntersectionObserver // Check if support is present for IntersectionObserver
if (!'IntersectionObserver' in window || if (!('IntersectionObserver' in window) ||
!'IntersectionObserverEntry' in window || !('IntersectionObserverEntry' in window) ||
!'intersectionRatio' in window.IntersectionObserverEntry.prototype) { !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
return; return;
} }
@ -208,8 +208,8 @@ class PageDisplay {
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts); let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading // observe each heading
for (let i = 0; i !== headings.length; ++i) { for (let heading of headings) {
pageNavObserver.observe(headings[i]); pageNavObserver.observe(heading);
} }
} }
@ -221,14 +221,9 @@ class PageDisplay {
} }
function toggleAnchorHighlighting(elementId, shouldHighlight) { function toggleAnchorHighlighting(elementId, shouldHighlight) {
let anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]'); const anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
for (let i = 0; i < anchorsToHighlight.length; i++) { for (let anchor of anchorsToHighlight) {
// Change below to use classList.toggle when IE support is dropped. anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
if (shouldHighlight) {
anchorsToHighlight[i].classList.add('current-heading');
} else {
anchorsToHighlight[i].classList.remove('current-heading');
}
} }
} }
} }

View File

@ -0,0 +1,66 @@
class PermissionsTable {
constructor(elem) {
this.container = elem;
// Handle toggle all event
const toggleAll = elem.querySelector('[permissions-table-toggle-all]');
toggleAll.addEventListener('click', this.toggleAllClick.bind(this));
// Handle toggle row event
const toggleRowElems = elem.querySelectorAll('[permissions-table-toggle-all-in-row]');
for (let toggleRowElem of toggleRowElems) {
toggleRowElem.addEventListener('click', this.toggleRowClick.bind(this));
}
// Handle toggle column event
const toggleColumnElems = elem.querySelectorAll('[permissions-table-toggle-all-in-column]');
for (let toggleColElem of toggleColumnElems) {
toggleColElem.addEventListener('click', this.toggleColumnClick.bind(this));
}
}
toggleAllClick(event) {
event.preventDefault();
this.toggleAllInElement(this.container);
}
toggleRowClick(event) {
event.preventDefault();
this.toggleAllInElement(event.target.closest('tr'));
}
toggleColumnClick(event) {
event.preventDefault();
const tableCell = event.target.closest('th,td');
const colIndex = Array.from(tableCell.parentElement.children).indexOf(tableCell);
const tableRows = tableCell.closest('table').querySelectorAll('tr');
const inputsToToggle = [];
for (let row of tableRows) {
const targetCell = row.children[colIndex];
if (targetCell) {
inputsToToggle.push(...targetCell.querySelectorAll('input[type=checkbox]'));
}
}
this.toggleAllInputs(inputsToToggle);
}
toggleAllInElement(domElem) {
const inputsToToggle = domElem.querySelectorAll('input[type=checkbox]');
this.toggleAllInputs(inputsToToggle);
}
toggleAllInputs(inputsToToggle) {
const currentState = inputsToToggle.length > 0 ? inputsToToggle[0].checked : false;
for (let checkbox of inputsToToggle) {
checkbox.checked = !currentState;
checkbox.dispatchEvent(new Event('change'));
}
}
}
export default PermissionsTable;

View File

@ -3,15 +3,15 @@ class ToggleSwitch {
constructor(elem) { constructor(elem) {
this.elem = elem; this.elem = elem;
this.input = elem.querySelector('input'); this.input = elem.querySelector('input[type=hidden]');
this.checkbox = elem.querySelector('input[type=checkbox]');
this.elem.onclick = this.onClick.bind(this); this.checkbox.addEventListener('change', this.onClick.bind(this));
} }
onClick(event) { onClick(event) {
let checked = this.input.value !== 'true'; let checked = this.checkbox.checked;
this.input.value = checked ? 'true' : 'false'; this.input.value = checked ? 'true' : 'false';
checked ? this.elem.classList.add('active') : this.elem.classList.remove('active');
} }
} }

View File

@ -0,0 +1,95 @@
class TriLayout {
constructor(elem) {
this.elem = elem;
this.lastLayoutType = 'none';
this.onDestroy = null;
this.scrollCache = {
'content': 0,
'info': 0,
};
this.lastTabShown = 'content';
// Bind any listeners
this.mobileTabClick = this.mobileTabClick.bind(this);
// Watch layout changes
this.updateLayout();
window.addEventListener('resize', event => {
this.updateLayout();
}, {passive: true});
}
updateLayout() {
let newLayout = 'tablet';
if (window.innerWidth <= 1000) newLayout = 'mobile';
if (window.innerWidth >= 1400) newLayout = 'desktop';
if (newLayout === this.lastLayoutType) return;
if (this.onDestroy) {
this.onDestroy();
this.onDestroy = null;
}
if (newLayout === 'desktop') {
this.setupDesktop();
} else if (newLayout === 'mobile') {
this.setupMobile();
}
this.lastLayoutType = newLayout;
}
setupMobile() {
const layoutTabs = document.querySelectorAll('[tri-layout-mobile-tab]');
for (let tab of layoutTabs) {
tab.addEventListener('click', this.mobileTabClick);
}
this.onDestroy = () => {
for (let tab of layoutTabs) {
tab.removeEventListener('click', this.mobileTabClick);
}
}
}
setupDesktop() {
//
}
/**
* Action to run when the mobile info toggle bar is clicked/tapped
* @param event
*/
mobileTabClick(event) {
const tab = event.target.getAttribute('tri-layout-mobile-tab');
this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
// Set tab status
const activeTabs = document.querySelectorAll('.tri-layout-mobile-tab.active');
for (let tab of activeTabs) {
tab.classList.remove('active');
}
event.target.classList.add('active');
// Toggle section
const showInfo = (tab === 'info');
this.elem.classList.toggle('show-info', showInfo);
// Set the scroll position from cache
const pageHeader = document.querySelector('header');
const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
setTimeout(() => {
document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
}, 50);
this.lastTabShown = tab;
}
}
export default TriLayout;

View File

@ -257,39 +257,38 @@ function drawIoPlugin() {
DrawIO.show(drawingInit, updateContent); DrawIO.show(drawingInit, updateContent);
} }
function updateContent(pngData) { async function updateContent(pngData) {
let id = "image-" + Math.random().toString(16).slice(2); const id = "image-" + Math.random().toString(16).slice(2);
let loadingImage = window.baseUrl('/loading.gif'); const loadingImage = window.baseUrl('/loading.gif');
let data = { const pageId = Number(document.getElementById('page-editor').getAttribute('page-id'));
image: pngData,
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
// Handle updating an existing image // Handle updating an existing image
if (currentNode) { if (currentNode) {
DrawIO.close(); DrawIO.close();
let imgElem = currentNode.querySelector('img'); let imgElem = currentNode.querySelector('img');
window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => { try {
pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url); const img = await DrawIO.upload(pngData, pageId);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id); pageEditor.dom.setAttrib(imgElem, 'src', img.url);
}).catch(err => { pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
} catch (err) {
window.$events.emit('error', trans('errors.image_upload_error')); window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err); console.log(err);
}); }
return; return;
} }
setTimeout(() => { setTimeout(async () => {
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`); pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
DrawIO.close(); DrawIO.close();
window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { try {
pageEditor.dom.setAttrib(id, 'src', resp.data.url); const img = await DrawIO.upload(pngData, pageId);
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); pageEditor.dom.setAttrib(id, 'src', img.url);
}).catch(err => { pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
} catch (err) {
pageEditor.dom.remove(id); pageEditor.dom.remove(id);
window.$events.emit('error', trans('errors.image_upload_error')); window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err); console.log(err);
}); }
}, 5); }, 5);
} }
@ -300,9 +299,7 @@ function drawIoPlugin() {
} }
let drawingId = currentNode.getAttribute('drawio-diagram'); let drawingId = currentNode.getAttribute('drawio-diagram');
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { return DrawIO.load(drawingId);
return `data:image/png;base64,${resp.data.content}`;
});
} }
window.tinymce.PluginManager.add('drawio', function(editor, url) { window.tinymce.PluginManager.add('drawio', function(editor, url) {
@ -432,7 +429,7 @@ class WysiwygEditor {
plugins: this.plugins, plugins: this.plugins,
imagetools_toolbar: 'imageoptions', imagetools_toolbar: 'imageoptions',
toolbar: this.getToolBar(), toolbar: this.getToolBar(),
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", content_style: "html, body {background: #FFF;} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [ style_formats: [
{title: "Header Large", format: "h2"}, {title: "Header Large", format: "h2"},
{title: "Header Medium", format: "h3"}, {title: "Header Medium", format: "h3"},
@ -517,6 +514,16 @@ class WysiwygEditor {
if (scrollId) { if (scrollId) {
scrollToText(scrollId); scrollToText(scrollId);
} }
// Override for touch events to allow scroll on mobile
const container = editor.getContainer();
const toolbarButtons = container.querySelectorAll('.mce-btn');
for (let button of toolbarButtons) {
button.addEventListener('touchstart', event => {
event.stopPropagation();
});
}
window.editor = editor;
}); });
function editorChange() { function editorChange() {
@ -600,6 +607,7 @@ class WysiwygEditor {
// Paste image-uploads // Paste image-uploads
editor.on('paste', event => editorPaste(event, editor, context)); editor.on('paste', event => editorPaste(event, editor, context));
} }
}; };
} }

View File

@ -8,13 +8,17 @@ import 'codemirror/mode/diff/diff';
import 'codemirror/mode/go/go'; import 'codemirror/mode/go/go';
import 'codemirror/mode/htmlmixed/htmlmixed'; import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/julia/julia';
import 'codemirror/mode/lua/lua'; import 'codemirror/mode/lua/lua';
import 'codemirror/mode/haskell/haskell';
import 'codemirror/mode/markdown/markdown'; import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/mllike/mllike';
import 'codemirror/mode/nginx/nginx'; import 'codemirror/mode/nginx/nginx';
import 'codemirror/mode/php/php'; import 'codemirror/mode/php/php';
import 'codemirror/mode/powershell/powershell'; import 'codemirror/mode/powershell/powershell';
import 'codemirror/mode/python/python'; import 'codemirror/mode/python/python';
import 'codemirror/mode/ruby/ruby'; import 'codemirror/mode/ruby/ruby';
import 'codemirror/mode/rust/rust';
import 'codemirror/mode/shell/shell'; import 'codemirror/mode/shell/shell';
import 'codemirror/mode/sql/sql'; import 'codemirror/mode/sql/sql';
import 'codemirror/mode/toml/toml'; import 'codemirror/mode/toml/toml';
@ -35,21 +39,29 @@ const modeMap = {
csharp: 'text/x-csharp', csharp: 'text/x-csharp',
diff: 'diff', diff: 'diff',
go: 'go', go: 'go',
haskell: 'haskell',
hs: 'haskell',
html: 'htmlmixed', html: 'htmlmixed',
javascript: 'javascript', javascript: 'javascript',
json: {name: 'javascript', json: true}, json: {name: 'javascript', json: true},
js: 'javascript', js: 'javascript',
jl: 'julia',
julia: 'julia',
lua: 'lua', lua: 'lua',
md: 'markdown', md: 'markdown',
mdown: 'markdown', mdown: 'markdown',
markdown: 'markdown', markdown: 'markdown',
ml: 'mllike',
nginx: 'nginx', nginx: 'nginx',
powershell: 'powershell', powershell: 'powershell',
ocaml: 'mllike',
php: 'php', php: 'php',
py: 'python', py: 'python',
python: 'python', python: 'python',
ruby: 'ruby', ruby: 'ruby',
rust: 'rust',
rb: 'ruby', rb: 'ruby',
rs: 'rust',
shell: 'shell', shell: 'shell',
sh: 'shell', sh: 'shell',
bash: 'shell', bash: 'shell',

View File

@ -66,4 +66,23 @@ function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
} }
export default {show, close}; async function upload(imageData, pageUploadedToId) {
let data = {
image: imageData,
uploaded_to: pageUploadedToId,
};
const resp = await window.$http.post(window.baseUrl(`/images/drawio`), data);
return resp.data;
}
/**
* Load an existing image, by fetching it as Base64 from the system.
* @param drawingId
* @returns {Promise<string>}
*/
async function load(drawingId) {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
}
export default {show, close, upload, load};

View File

@ -1,7 +1,7 @@
import * as Dates from "../services/dates"; import * as Dates from "../services/dates";
import dropzone from "./components/dropzone"; import dropzone from "./components/dropzone";
let page = 0; let page = 1;
let previousClickTime = 0; let previousClickTime = 0;
let previousClickImage = 0; let previousClickImage = 0;
let dataLoaded = false; let dataLoaded = false;
@ -20,7 +20,7 @@ const data = {
selectedImage: false, selectedImage: false,
dependantPages: false, dependantPages: false,
showing: false, showing: false,
view: 'all', filter: null,
hasMore: false, hasMore: false,
searching: false, searching: false,
searchTerm: '', searchTerm: '',
@ -56,32 +56,37 @@ const methods = {
this.$el.children[0].components.overlay.hide(); this.$el.children[0].components.overlay.hide();
}, },
fetchData() { async fetchData() {
let url = baseUrl + page; let query = {
let query = {}; page,
if (this.uploadedTo !== false) query.page_id = this.uploadedTo; search: this.searching ? this.searchTerm : null,
if (this.searching) query.term = this.searchTerm; uploaded_to: this.uploadedTo || null,
filter_type: this.filter,
};
this.$http.get(url, {params: query}).then(response => { const {data} = await this.$http.get(baseUrl, {params: query});
this.images = this.images.concat(response.data.images); this.images = this.images.concat(data.images);
this.hasMore = response.data.hasMore; this.hasMore = data.has_more;
page++; page++;
});
}, },
setView(viewName) { setFilterType(filterType) {
this.view = viewName; this.filter = filterType;
this.resetState(); this.resetState();
this.fetchData(); this.fetchData();
}, },
resetState() { resetState() {
this.cancelSearch(); this.cancelSearch();
this.resetListView();
this.deleteConfirm = false;
baseUrl = window.baseUrl(`/images/${this.imageType}`);
},
resetListView() {
this.images = []; this.images = [];
this.hasMore = false; this.hasMore = false;
this.deleteConfirm = false; page = 1;
page = 0;
baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`);
}, },
searchImages() { searchImages() {
@ -94,10 +99,7 @@ const methods = {
} }
this.searching = true; this.searching = true;
this.images = []; this.resetListView();
this.hasMore = false;
page = 0;
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
this.fetchData(); this.fetchData();
}, },
@ -110,10 +112,10 @@ const methods = {
}, },
imageSelect(image) { imageSelect(image) {
let dblClickTime = 300; const dblClickTime = 300;
let currentTime = Date.now(); const currentTime = Date.now();
let timeDiff = currentTime - previousClickTime; const timeDiff = currentTime - previousClickTime;
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
if (isDblClick) { if (isDblClick) {
this.callbackAndHide(image); this.callbackAndHide(image);
@ -132,11 +134,11 @@ const methods = {
this.hide(); this.hide();
}, },
saveImageDetails() { async saveImageDetails() {
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`); let url = window.baseUrl(`/images/${this.selectedImage.id}`);
this.$http.put(url, this.selectedImage).then(response => { try {
this.$events.emit('success', trans('components.image_update_success')); await this.$http.put(url, this.selectedImage)
}).catch(error => { } catch (error) {
if (error.response.status === 422) { if (error.response.status === 422) {
let errors = error.response.data; let errors = error.response.data;
let message = ''; let message = '';
@ -145,27 +147,29 @@ const methods = {
}); });
this.$events.emit('error', message); this.$events.emit('error', message);
} }
}); }
}, },
deleteImage() { async deleteImage() {
if (!this.deleteConfirm) { if (!this.deleteConfirm) {
let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`); const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
this.$http.get(url).then(resp => { try {
this.dependantPages = resp.data; const {data} = await this.$http.get(url);
}).catch(console.error).then(() => { this.dependantPages = data;
} catch (error) {
console.error(error);
}
this.deleteConfirm = true; this.deleteConfirm = true;
});
return; return;
} }
let url = window.baseUrl(`/images/${this.selectedImage.id}`);
this.$http.delete(url).then(resp => { const url = window.baseUrl(`/images/${this.selectedImage.id}`);
await this.$http.delete(url);
this.images.splice(this.images.indexOf(this.selectedImage), 1); this.images.splice(this.images.indexOf(this.selectedImage), 1);
this.selectedImage = false; this.selectedImage = false;
this.$events.emit('success', trans('components.image_delete_success')); this.$events.emit('success', trans('components.image_delete_success'));
this.deleteConfirm = false; this.deleteConfirm = false;
});
}, },
getDate(stringDate) { getDate(stringDate) {
@ -180,7 +184,7 @@ const methods = {
const computed = { const computed = {
uploadUrl() { uploadUrl() {
return window.baseUrl(`/images/${this.imageType}/upload`); return window.baseUrl(`/images/${this.imageType}`);
} }
}; };
@ -188,7 +192,7 @@ function mounted() {
window.ImageManager = this; window.ImageManager = this;
this.imageType = this.$el.getAttribute('image-type'); this.imageType = this.$el.getAttribute('image-type');
this.uploadedTo = this.$el.getAttribute('uploaded-to'); this.uploadedTo = this.$el.getAttribute('uploaded-to');
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/') baseUrl = window.baseUrl('/images/' + this.imageType)
} }
export default { export default {

View File

@ -36,26 +36,6 @@
} }
} }
.anim.menuIn {
transform-origin: 100% 0%;
animation-name: menuIn;
animation-duration: 120ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
@keyframes menuIn {
from {
opacity: 0;
transform: scale3d(0, 0, 1);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
@keyframes loadingBob { @keyframes loadingBob {
0% { 0% {
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
@ -90,7 +70,3 @@
animation-delay: 0s; animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99); animation-timing-function: cubic-bezier(.62, .28, .23, .99);
} }
.selectFade {
transition: background-color ease-in-out 3000ms;
}

View File

@ -1,136 +1,7 @@
/*
* This file container all block styling including background shading,
* margins, paddings & borders.
*/
/*
* Background Shading
*/
.shaded {
background-color: #f1f1f1;
&.pos {
background-color: lighten($positive, 40%);
}
&.neg {
background-color: lighten($negative, 20%);
}
&.primary {
background-color: lighten($primary, 40%);
}
&.secondary {
background-color: lighten($secondary, 30%);
}
}
/*
* Bordering
*/
.bordered {
border: 1px solid #BBB;
&.pos {
border-color: $positive;
}
&.neg {
border-color: $negative;
}
&.primary {
border-color: $primary;
}
&.secondary {
border-color: $secondary;
}
&.thick {
border-width: 2px;
}
}
.rounded {
border-radius: 3px;
}
/*
* Padding
*/
.nopadding {
padding: 0;
}
.padded {
padding: $-l;
&.large {
padding: $-xl;
}
>h1, >h2, >h3, >h4 {
&:first-child {
margin-top: 0.1em;
}
}
}
.padded-vertical, .padded-top {
padding-top: $-m;
&.large {
padding-top: $-xl;
}
}
.padded-vertical, .padded-bottom {
padding-bottom: $-m;
&.large {
padding-bottom: $-xl;
}
}
.padded-horizontal, .padded-left {
padding-left: $-m;
&.large {
padding-left: $-xl;
}
}
.padded-horizontal, .padded-right {
padding-right: $-m;
&.large {
padding-right: $-xl;
}
}
/*
* Margins
*/
.margins {
margin: $-l;
&.large {
margin: $-xl;
}
}
.margins-vertical, .margin-top {
margin-top: $-m;
&.large {
margin-top: $-xl;
}
}
.margins-vertical, .margin-bottom {
margin-bottom: $-m;
&.large {
margin-bottom: $-xl;
}
}
.margins-horizontal, .margin-left {
margin-left: $-m;
&.large {
margin-left: $-xl;
}
}
.margins-horizontal, .margin-right {
margin-right: $-m;
&.large {
margin-right: $-xl;
}
}
/** /**
* Callouts * Callouts
*/ */
.callout { .callout {
border-left: 3px solid #BBB; border-left: 3px solid #BBB;
background-color: #EEE; background-color: #EEE;
@ -143,7 +14,7 @@
content: ''; content: '';
width: 1.2em; width: 1.2em;
height: 1.2em; height: 1.2em;
left: $-xs + 1px; left: $-xs + 2px;
top: 50%; top: 50%;
margin-top: -9px; margin-top: -9px;
display: inline-block; display: inline-block;
@ -153,7 +24,7 @@
} }
&.success { &.success {
border-left-color: $positive; border-left-color: $positive;
background-color: lighten($positive, 45%); background-color: lighten($positive, 68%);
color: darken($positive, 16%); color: darken($positive, 16%);
} }
&.success:before { &.success:before {
@ -161,7 +32,7 @@
} }
&.danger { &.danger {
border-left-color: $negative; border-left-color: $negative;
background-color: lighten($negative, 34%); background-color: lighten($negative, 56%);
color: darken($negative, 20%); color: darken($negative, 20%);
} }
&.danger:before { &.danger:before {
@ -170,35 +41,27 @@
&.info { &.info {
border-left-color: $info; border-left-color: $info;
background-color: lighten($info, 50%); background-color: lighten($info, 50%);
color: darken($info, 16%); color: darken($info, 20%);
} }
&.warning { &.warning {
border-left-color: $warning; border-left-color: $warning;
background-color: lighten($warning, 36%); background-color: lighten($warning, 50%);
color: darken($warning, 16%); color: darken($warning, 20%);
} }
&.warning:before { &.warning:before {
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4="); background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
} }
} }
/**
* Card-style blocks
*/
.card { .card {
margin: $-m;
background-color: #FFF; background-color: #FFF;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2); box-shadow: $bs-card;
h3 { border-radius: 3px;
padding: $-m; border: 1px solid transparent;
border-bottom: 1px solid #E8E8E8;
margin: 0;
font-size: $fs-s;
color: #888;
fill: #888;
font-weight: 400;
text-transform: uppercase;
}
h3 a {
line-height: 1;
}
.body, p.empty-text { .body, p.empty-text {
padding: $-m; padding: $-m;
} }
@ -208,18 +71,23 @@
} }
} }
.sidebar .card { .card-title {
h3, .body, .empty-text { padding: $-m $-m $-xs;
padding: $-s $-m; margin: 0;
} font-size: $fs-m;
color: #222;
fill: #222;
font-weight: 400;
}
.card-title a {
line-height: 1;
} }
.card.drag-card { .card.drag-card {
border: 1px solid #DDD; border: 1px solid #DDD;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
padding: 0; padding: 0 0 0 ($-s + 28px);
padding-left: $-s + 28px;
margin: $-s 0; margin: $-s 0;
position: relative; position: relative;
.drag-card-action { .drag-card-action {
@ -227,14 +95,12 @@
} }
.handle, .drag-card-action { .handle, .drag-card-action {
display: flex; display: flex;
padding: 0;
align-items: center; align-items: center;
text-align: center; text-align: center;
justify-content: center; justify-content: center;
width: 28px; width: 28px;
flex-grow: 0; flex-grow: 0;
padding-left: $-xs; padding: 0 $-xs;
padding-right: $-xs;
&:hover { &:hover {
background-color: #EEE; background-color: #EEE;
} }
@ -246,9 +112,6 @@
margin: $-s 0; margin: $-s 0;
width: 100%; width: 100%;
} }
> div.padded {
padding: $-s 0 !important;
}
.handle { .handle {
background-color: #EEE; background-color: #EEE;
left: 0; left: 0;
@ -263,12 +126,89 @@
} }
} }
.well { .grid-card {
background-color: #F8F8F8; display: flex;
padding: $-m; flex-direction: column;
border: 1px solid #DDD; border: 1px solid #ddd;
margin-bottom: $-l;
border-radius: 4px;
overflow: hidden;
min-width: 100px;
color: $text-dark;
transition: border-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
&:hover {
color: $text-dark;
text-decoration: none;
box-shadow: $bs-card;
}
h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px;
}
p {
font-size: .7rem;
margin: 0;
line-height: 1.6em;
}
.grid-card-content {
flex: 1;
border-top: 0;
border-bottom-width: 2px;
}
.grid-card-content, .grid-card-footer {
padding: $-l;
}
.grid-card-content + .grid-card-footer {
padding-top: 0;
}
} }
.bookshelf-grid-item .grid-card-content h2 a {
color: $color-bookshelf;
fill: $color-bookshelf;
}
.book-grid-item .grid-card-footer {
p.small {
font-size: .8em;
margin: 0;
}
}
.content-wrap.card {
padding: $-m $-xxl;
margin-left: auto;
margin-right: auto;
margin-bottom: $-xl;
overflow: auto;
min-height: 60vh;
&.auto-height {
min-height: 0;
}
&.fill-width {
width: 100%;
}
}
@include smaller-than($xxl) {
.content-wrap.card {
padding: $-l $-xl;
}
}
@include smaller-than($m) {
.content-wrap.card {
padding: $-m $-l;
}
}
@include smaller-than($s) {
.content-wrap.card {
padding: $-m $-s;
}
}
/**
* Tags
*/
.tag-item { .tag-item {
display: inline-flex; display: inline-flex;
margin-bottom: $-xs; margin-bottom: $-xs;

View File

@ -1,15 +1,14 @@
button {
font-size: 100%;
}
@mixin generate-button-colors($textColor, $backgroundColor) { @mixin generate-button-colors($textColor, $backgroundColor) {
background-color: $backgroundColor; background-color: $backgroundColor;
color: $textColor; color: $textColor;
fill: $textColor; fill: $textColor;
text-transform: uppercase;
border: 1px solid $backgroundColor; border: 1px solid $backgroundColor;
vertical-align: top;
&:hover { &:hover {
background-color: lighten($backgroundColor, 8%); background-color: lighten($backgroundColor, 8%);
//box-shadow: $bs-med;
text-decoration: none;
color: $textColor; color: $textColor;
} }
&:active { &:active {
@ -18,7 +17,6 @@
&:focus { &:focus {
background-color: lighten($backgroundColor, 4%); background-color: lighten($backgroundColor, 4%);
box-shadow: $bs-light; box-shadow: $bs-light;
text-decoration: none;
color: $textColor; color: $textColor;
} }
} }
@ -26,42 +24,36 @@
// Button Specific Variables // Button Specific Variables
$button-border-radius: 2px; $button-border-radius: 2px;
.button-base { .button {
text-decoration: none; text-decoration: none;
font-size: $fs-m; font-size: 0.85rem;
line-height: 1.4em; line-height: 1.4em;
padding: $-xs*1.3 $-m; padding: $-xs*1.3 $-m;
margin: $-xs $-xs $-xs 0; margin-top: $-xs;
margin-bottom: $-xs;
display: inline-block; display: inline-block;
border: none;
font-weight: 400; font-weight: 400;
outline: 0; outline: 0;
border-radius: $button-border-radius; border-radius: $button-border-radius;
cursor: pointer; cursor: pointer;
transition: all ease-in-out 120ms; transition: background-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
box-shadow: 0; box-shadow: none;
@include generate-button-colors(#EEE, $primary); background-color: $primary;
} color: #FFF;
fill: #FFF;
.button, input[type="button"], input[type="submit"] { text-transform: uppercase;
@extend .button-base; border: 1px solid $primary;
&.pos { vertical-align: top;
@include generate-button-colors(#EEE, $positive); &:hover, &:focus {
text-decoration: none;
} }
&.neg { &:active {
@include generate-button-colors(#EEE, $negative); background-color: darken($primary, 8%);
}
&.secondary {
@include generate-button-colors(#EEE, $secondary);
}
&.muted {
@include generate-button-colors(#EEE, #AAA);
}
&.muted-light {
@include generate-button-colors(#666, #e4e4e4);
} }
} }
.button.primary {
@include generate-button-colors(#FFFFFF, $primary);
}
.button.outline { .button.outline {
background-color: transparent; background-color: transparent;
color: #888; color: #888;
@ -71,78 +63,38 @@ $button-border-radius: 2px;
box-shadow: none; box-shadow: none;
background-color: #EEE; background-color: #EEE;
} }
&.page { }
border-color: $color-page;
color: $color-page; .button + .button {
fill: $color-page; margin-left: $-s;
&:hover, &:focus, &:active { }
background-color: $color-page;
color: #FFF; .button.small {
fill: #FFF; font-size: 0.75rem;
} padding: $-xs*1.2 $-s;
}
&.chapter {
border-color: $color-chapter;
color: $color-chapter;
fill: $color-chapter;
&:hover, &:focus, &:active {
background-color: $color-chapter;
color: #FFF;
fill: #FFF;
}
}
&.book {
border-color: $color-book;
color: $color-book;
fill: $color-book;
&:hover, &:focus, &:active {
background-color: $color-book;
color: #FFF;
fill: #FFF;
}
}
} }
.text-button { .text-button {
@extend .link; cursor: pointer;
background-color: transparent; background-color: transparent;
padding: 0; padding: 0;
margin: 0; margin: 0;
border: none; border: none;
user-select: none; user-select: none;
font-size: 0.75rem;
line-height: 1.4em;
&:focus, &:active { &:focus, &:active {
outline: 0; outline: 0;
} }
&:hover { &:hover {
text-decoration: none; text-decoration: none;
} }
&.neg {
color: $negative;
}
}
.button-group {
@include clearfix;
.button, button[type="button"] {
margin: $-xs 0 $-xs 0;
float: left;
border-radius: 0;
&:first-child {
border-radius: $button-border-radius 0 0 $button-border-radius;
}
&:last-child {
border-radius: 0 $button-border-radius $button-border-radius 0;
}
}
} }
.button.block { .button.block {
width: 100%; width: 100%;
text-align: center;
display: block;
&.text-left {
text-align: left; text-align: left;
} display: block;
} }
.button.icon { .button.icon {
@ -160,9 +112,7 @@ $button-border-radius: 2px;
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
padding: $-s $-m; padding: $-s $-m ($-s - 2px) ($-m*2 + 24px);
padding-bottom: $-s - 2px;
padding-left: $-m*2 + 24px;
} }
.button[disabled] { .button[disabled] {

View File

@ -0,0 +1,72 @@
/*
* Status text colors
*/
.text-pos, .text-pos:hover, .text-pos-hover:hover {
color: $positive !important;
fill: $positive !important;
}
.text-warn, .text-warn:hover, .text-warn-hover:hover {
color: $warning !important;
fill: $warning !important;
}
.text-neg, .text-neg:hover, .text-neg-hover:hover {
color: $negative !important;
fill: $negative !important;
}
/*
* Style text colors
*/
.text-primary, .text-primary:hover, .text-primary-hover:hover {
color: $primary !important;
fill: $primary !important;
}
.text-muted {
color: lighten($text-dark, 26%) !important;
fill: lighten($text-dark, 26%) !important;
&.small, .small {
color: lighten($text-dark, 32%) !important;
fill: lighten($text-dark, 32%) !important;
}
}
/*
* Entity text colors
*/
.text-bookshelf, .text-bookshelf:hover {
color: $color-bookshelf;
fill: $color-bookshelf;
}
.text-book, .text-book:hover {
color: $color-book;
fill: $color-book;
}
.text-page, .text-page:hover {
color: $color-page;
fill: $color-page;
}
.text-page.draft, .text-page.draft:hover {
color: $color-page-draft;
fill: $color-page-draft;
}
.text-chapter, .text-chapter:hover {
color: $color-chapter;
fill: $color-chapter;
}
/*
* Entity background colors
*/
.bg-book {
background-color: $color-book;
}
.bg-chapter {
background-color: $color-chapter;
}
.bg-shelf {
background-color: $color-bookshelf;
}

View File

@ -3,11 +3,12 @@
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
margin: $-xl*2 $-xl; margin: $-xl;
padding: $-m $-l; padding: $-m $-l;
background-color: #EEE; background-color: #FFF;
border-radius: 3px; border-radius: 4px;
box-shadow: $bs-card; border-left: 6px solid currentColor;
box-shadow: $bs-large;
z-index: 999999; z-index: 999999;
cursor: pointer; cursor: pointer;
max-width: 360px; max-width: 360px;
@ -15,30 +16,31 @@
transform: translateX(580px); transform: translateX(580px);
display: grid; display: grid;
grid-template-columns: 42px 1fr; grid-template-columns: 42px 1fr;
color: #FFF; color: #444;
font-weight: 700;
span, svg { span, svg {
vertical-align: middle; vertical-align: middle;
justify-self: center; justify-self: center;
align-self: center; align-self: center;
} }
svg { svg {
fill: #EEEEEE;
width: 2.8rem; width: 2.8rem;
height: 2.8rem; height: 2.8rem;
padding-right: $-s; padding-right: $-s;
fill: currentColor;
} }
span { span {
vertical-align: middle; vertical-align: middle;
line-height: 1.3; line-height: 1.3;
} }
&.pos { &.pos {
background-color: $positive; color: $positive;
} }
&.neg { &.neg {
background-color: $negative; color: $negative;
} }
&.warning { &.warning {
background-color: $secondary; color: $warning;
} }
&.showing { &.showing {
transform: translateX(0); transform: translateX(0);
@ -54,13 +56,18 @@
transition: all ease-in-out 180ms; transition: all ease-in-out 180ms;
user-select: none; user-select: none;
svg[data-icon="caret-right"] { svg[data-icon="caret-right"] {
margin-right: 0;
font-size: 1rem;
transition: all ease-in-out 180ms; transition: all ease-in-out 180ms;
transform: rotate(0deg); transform: rotate(0deg);
transform-origin: 25% 50%; transform-origin: 50% 50%;
} }
&.open svg[data-icon="caret-right"] { &.open svg[data-icon="caret-right"] {
transform: rotate(90deg); transform: rotate(90deg);
} }
svg[data-icon="caret-right"] + * {
margin-left: $-xs;
}
} }
[overlay] { [overlay] {
@ -110,7 +117,7 @@
} }
} }
.corner-button { .popup-footer button, .popup-header-close {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@ -118,6 +125,16 @@
height: 40px; height: 40px;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
&:active {
outline: 0;
}
}
.popup-header-close {
background-color: transparent;
border: 0;
color: #FFF;
font-size: 16px;
padding: 0 $-m;
} }
.popup-header, .popup-footer { .popup-header, .popup-footer {
@ -130,6 +147,9 @@
padding: 8px $-m; padding: 8px $-m;
} }
} }
.popup-footer {
margin-top: 1px;
}
body.flexbox-support #entity-selector-wrap .popup-body .form-group { body.flexbox-support #entity-selector-wrap .popup-body .form-group {
height: 444px; height: 444px;
min-height: 444px; min-height: 444px;
@ -137,6 +157,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
#entity-selector-wrap .popup-body .form-group { #entity-selector-wrap .popup-body .form-group {
margin: 0; margin: 0;
} }
.popup-body .entity-selector-container {
flex: 1;
}
.image-manager-body { .image-manager-body {
min-height: 70vh; min-height: 70vh;
@ -583,27 +606,26 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
} }
.comment-box { .comment-box {
clear: left;
border: 1px solid #DDD; border: 1px solid #DDD;
margin-bottom: $-s; border-radius: 4px;
border-radius: 3px; background-color: #FFF;
.content { .content {
padding: $-s;
font-size: 0.666em; font-size: 0.666em;
p, ul, ol { p, ul, ol {
font-size: $fs-m; font-size: $fs-m;
margin: .5em 0; margin: .5em 0;
} }
} }
.reply-row { .actions {
padding: $-xs $-s; opacity: 0;
transition: opacity ease-in-out 120ms;
}
&:hover .actions {
opacity: 1;
} }
} }
.comment-box .header { .comment-box .header {
padding: $-xs $-s;
background-color: #f8f8f8;
border-bottom: 1px solid #DDD;
.meta { .meta {
img, a, span { img, a, span {
display: inline-block; display: inline-block;
@ -627,3 +649,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
#tag-manager .drag-card { #tag-manager .drag-card {
max-width: 500px; max-width: 500px;
} }
.permissions-table [permissions-table-toggle-all-in-row] {
display: none;
}
.permissions-table tr:hover [permissions-table-toggle-all-in-row] {
display: inline;
}

View File

@ -63,6 +63,34 @@
} }
} }
@include smaller-than($m) {
#markdown-editor {
flex-direction: column;
}
#markdown-editor .markdown-editor-wrap {
width: 100%;
max-width: 100%;
}
#markdown-editor .editor-toolbar {
padding: 0;
}
#markdown-editor .editor-toolbar > * {
padding: $-xs $-s;
}
.editor-toolbar-label {
float: none !important;
border-bottom: 1px solid #DDD;
display: block;
}
.markdown-editor-wrap:not(.active) .editor-toolbar + div, .markdown-editor-wrap:not(.active) .editor-toolbar .buttons {
display: none;
}
#markdown-editor .markdown-editor-wrap:not(.active) {
flex-grow: 0;
flex: none;
}
}
.markdown-display { .markdown-display {
padding: 0 $-m 0; padding: 0 $-m 0;
margin-left: -1px; margin-left: -1px;
@ -98,7 +126,7 @@ label {
line-height: 1.4em; line-height: 1.4em;
font-size: 0.94em; font-size: 0.94em;
font-weight: 400; font-weight: 400;
color: #999; color: #666;
padding-bottom: 2px; padding-bottom: 2px;
margin-bottom: 0.2em; margin-bottom: 0.2em;
&.inline { &.inline {
@ -139,56 +167,77 @@ input[type=date] {
} }
.toggle-switch { .toggle-switch {
display: inline-block;
background-color: #BBB;
width: 36px;
height: 14px;
border-radius: 7px;
position: relative;
transition: all ease-in-out 120ms;
cursor: pointer;
user-select: none; user-select: none;
&:after { display: inline-grid;
content: ''; grid-template-columns: (16px + $-s) 1fr;
display: block; align-items: center;
position: relative; margin: $-m 0;
.custom-checkbox {
width: 16px;
height: 16px;
border-radius: 2px;
display: inline-block;
border: 2px solid currentColor;
opacity: 0.6;
overflow: hidden;
fill: currentColor;
.svg-icon {
width: 100%;
height: 100%;
margin: 0;
bottom: auto;
top: -1.5px;
left: 0; left: 0;
margin-top: -3px; transition: transform ease-in-out 120ms;
width: 20px; transform: scale(0);
height: 20px; transform-origin: center center;
border-radius: 50%;
background-color: #fafafa;
border: 1px solid #CCC;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
transition: all ease-in-out 120ms;
}
&.active {
background-color: rgba($positive, 0.4);
&:after {
left: 16px;
background-color: $positive;
border: darken($positive, 20%);
} }
} }
} input[type=checkbox] {
.toggle-switch-checkbox {
display: none; display: none;
}
input[type=checkbox]:checked + .custom-checkbox .svg-icon {
transform: scale(1);
}
.custom-checkbox:hover {
background-color: rgba(0, 0, 0, 0.05);
opacity: 0.8;
}
} }
input:checked + .toggle-switch { .toggle-switch-list {
background-color: rgba($positive, 0.4); .toggle-switch {
&:after { margin: $-xs 0;
left: 16px; }
background-color: $positive; &.compact .toggle-switch {
border: darken($positive, 20%); margin: 1px 0;
} }
} }
.form-group { .form-group {
margin-bottom: $-s; margin-bottom: $-s;
textarea { }
display: block;
.setting-list > div {
border-bottom: 1px solid #DDD;
padding: $-xl 0;
&:last-child {
border-bottom: none;
}
}
.setting-list-label {
color: #222;
font-size: 1rem;
}
.setting-list-label + p.small {
margin-bottom: 0;
}
.setting-list-label + .grid {
margin-top: $-m;
}
.setting-list .grid, .stretch-inputs {
input[type=text], input[type=email], input[type=password], select {
width: 100%; width: 100%;
min-height: 64px;
} }
} }
@ -197,20 +246,20 @@ input:checked + .toggle-switch {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
min-height: 100px; min-height: 100px;
display: block;
width: 100%;
} }
.form-group { .form-group {
.text-pos, .text-neg { div.text-pos, div.text-neg, p.text-post, p.text-neg {
padding: $-xs 0; padding: $-xs 0;
} }
} }
.form-group[collapsible] { .form-group[collapsible] {
margin-left: -$-m;
margin-right: -$-m;
padding: 0 $-m; padding: 0 $-m;
border-top: 1px solid #DDD; border: 1px solid #DDD;
border-bottom: 1px solid #DDD; border-radius: 4px;
.collapse-title { .collapse-title {
margin-left: -$-m; margin-left: -$-m;
margin-right: -$-m; margin-right: -$-m;
@ -238,9 +287,6 @@ input:checked + .toggle-switch {
&.open .collapse-title label:before { &.open .collapse-title label:before {
transform: rotate(90deg); transform: rotate(90deg);
} }
&+.form-group[collapsible] {
margin-top: -($-s + 1);
}
} }
.inline-input-style { .inline-input-style {
@ -304,6 +350,13 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
width: 300px; width: 300px;
max-width: 100%; max-width: 100%;
} }
&.flexible input {
width: 100%;
}
.search-box-cancel {
left: auto;
right: 0;
}
} }
.outline > input { .outline > input {
@ -317,13 +370,8 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
} }
} }
#login-form label[for="remember"] {
margin: 0;
}
#login-form label.toggle-switch {
margin-left: $-xl;
}
.image-picker img { .image-picker img {
background-color: #BBB; background-color: #BBB;
max-width: 100%;
} }

View File

@ -1,930 +0,0 @@
/** Flexbox styling rules **/
body.flexbox {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
min-height: 100%;
max-height: 100%;
overflow: hidden;
#content {
flex: 1;
display: flex;
min-height: 0;
}
}
.flex-fill {
display: flex;
align-items: stretch;
min-height: 0;
max-width: 100%;
position: relative;
&.rows {
flex-direction: row;
}
&.columns {
flex-direction: column;
}
}
.flex {
min-height: 0;
flex: 1;
}
.flex.scroll {
//overflow-y: auto;
display: flex;
&.sidebar {
margin-right: -14px;
}
}
.flex.scroll .scroll-body {
overflow-y: scroll;
flex: 1;
}
.flex-child > div {
flex: 1;
}
.flex.sidebar {
flex: 1;
background-color: #F2F2F2;
max-width: 360px;
min-height: 90vh;
section {
margin: $-m;
}
}
.flex.sidebar + .flex.content {
flex: 3;
background-color: #FFFFFF;
padding: 0 $-l;
border-left: 1px solid #DDD;
max-width: 100%;
}
.flex.sidebar .sidebar-toggle {
display: none;
}
@include smaller-than($xl) {
body.sidebar-layout {
padding-left: 30px;
}
.flex.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 100;
padding-right: 30px;
width: 360px;
box-shadow: none;
transform: translate3d(-330px, 0, 0);
transition: transform ease-in-out 120ms;
display: flex;
flex-direction: column;
}
.flex.sidebar.open {
box-shadow: 1px 2px 2px 1px rgba(0,0,0,.10);
transform: translate3d(0, 0, 0);
.sidebar-toggle i {
transform: rotate(180deg);
}
}
.flex.sidebar .sidebar-toggle {
display: block;
position: absolute;
opacity: 0.9;
right: 0;
top: 0;
bottom: 0;
width: 30px;
fill: #666;
font-size: 20px;
vertical-align: middle;
text-align: center;
border: 1px solid #DDD;
border-top: 1px solid #BBB;
padding-top: $-m;
cursor: pointer;
svg {
opacity: 0.5;
transition: all ease-in-out 120ms;
margin: 0;
}
&:hover i {
opacity: 1;
}
}
.sidebar .scroll-body {
flex: 1;
overflow-y: scroll;
}
#sidebar .scroll-body.fixed {
width: auto !important;
}
}
@include larger-than($xl) {
#sidebar .scroll-body.fixed {
z-index: 5;
position: fixed;
top: 0;
padding-right: $-m;
width: 30%;
left: 0;
height: 100%;
overflow-y: auto;
-ms-overflow-style: none;
//background-color: $primary-faded;
border-left: 1px solid #DDD;
&::-webkit-scrollbar { width: 0 !important }
}
}
/** Rules for all columns */
div[class^="col-"] img {
max-width: 100%;
}
.container {
max-width: $max-width;
margin-left: auto;
margin-right: auto;
padding-left: $-m;
padding-right: $-m;
&.fluid {
max-width: 100%;
}
&.medium {
max-width: 992px;
}
&.small {
max-width: 840px;
}
&.nopad {
padding-left: 0;
padding-right: 0;
}
}
.row {
margin-left: -$-m;
margin-right: -$-m;
}
.grid {
display: grid;
grid-column-gap: $-l;
grid-row-gap: $-l;
&.third {
grid-template-columns: 1fr 1fr 1fr;
}
}
.grid-card {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
min-width: 100px;
h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px;
}
h2 a {
display: block;
width: 100%;
line-height: 1.2;
text-decoration: none;
}
p {
font-size: .85em;
margin: 0;
line-height: 1.6em;
}
.grid-card-content {
flex: 1;
border-top: 0;
border-bottom-width: 2px;
}
.grid-card-content, .grid-card-footer {
padding: $-l;
}
.grid-card-content + .grid-card-footer {
padding-top: 0;
}
}
.book-grid-item .grid-card-content h2 a {
color: $color-book;
fill: $color-book;
}
.bookshelf-grid-item .grid-card-content h2 a {
color: $color-bookshelf;
fill: $color-bookshelf;
}
.book-grid-item .grid-card-footer {
p.small {
font-size: .8em;
margin: 0;
}
}
@include smaller-than($m) {
.grid.third {
grid-template-columns: 1fr 1fr;
}
}
@include smaller-than($s) {
.grid.third {
grid-template-columns: 1fr;
}
}
.float {
float: left;
&.right {
float: right;
}
}
.block {
display: block;
position: relative;
}
.inline {
display: inline;
}
.block.inline {
display: inline-block;
}
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
position: relative;
min-height: 1px;
padding-left: $-m;
padding-right: $-m;
}
.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
float: left;
}
.col-xs-12 {
width: 100%;
}
.col-xs-11 {
width: 91.66666667%;
}
.col-xs-10 {
width: 83.33333333%;
}
.col-xs-9 {
width: 75%;
}
.col-xs-8 {
width: 66.66666667%;
}
.col-xs-7 {
width: 58.33333333%;
}
.col-xs-6 {
width: 50%;
}
.col-xs-5 {
width: 41.66666667%;
}
.col-xs-4 {
width: 33.33333333%;
}
.col-xs-3 {
width: 25%;
}
.col-xs-2 {
width: 16.66666667%;
}
.col-xs-1 {
width: 8.33333333%;
}
.col-xs-pull-12 {
right: 100%;
}
.col-xs-pull-11 {
right: 91.66666667%;
}
.col-xs-pull-10 {
right: 83.33333333%;
}
.col-xs-pull-9 {
right: 75%;
}
.col-xs-pull-8 {
right: 66.66666667%;
}
.col-xs-pull-7 {
right: 58.33333333%;
}
.col-xs-pull-6 {
right: 50%;
}
.col-xs-pull-5 {
right: 41.66666667%;
}
.col-xs-pull-4 {
right: 33.33333333%;
}
.col-xs-pull-3 {
right: 25%;
}
.col-xs-pull-2 {
right: 16.66666667%;
}
.col-xs-pull-1 {
right: 8.33333333%;
}
.col-xs-pull-0 {
right: auto;
}
.col-xs-push-12 {
left: 100%;
}
.col-xs-push-11 {
left: 91.66666667%;
}
.col-xs-push-10 {
left: 83.33333333%;
}
.col-xs-push-9 {
left: 75%;
}
.col-xs-push-8 {
left: 66.66666667%;
}
.col-xs-push-7 {
left: 58.33333333%;
}
.col-xs-push-6 {
left: 50%;
}
.col-xs-push-5 {
left: 41.66666667%;
}
.col-xs-push-4 {
left: 33.33333333%;
}
.col-xs-push-3 {
left: 25%;
}
.col-xs-push-2 {
left: 16.66666667%;
}
.col-xs-push-1 {
left: 8.33333333%;
}
.col-xs-push-0 {
left: auto;
}
.col-xs-offset-12 {
margin-left: 100%;
}
.col-xs-offset-11 {
margin-left: 91.66666667%;
}
.col-xs-offset-10 {
margin-left: 83.33333333%;
}
.col-xs-offset-9 {
margin-left: 75%;
}
.col-xs-offset-8 {
margin-left: 66.66666667%;
}
.col-xs-offset-7 {
margin-left: 58.33333333%;
}
.col-xs-offset-6 {
margin-left: 50%;
}
.col-xs-offset-5 {
margin-left: 41.66666667%;
}
.col-xs-offset-4 {
margin-left: 33.33333333%;
}
.col-xs-offset-3 {
margin-left: 25%;
}
.col-xs-offset-2 {
margin-left: 16.66666667%;
}
.col-xs-offset-1 {
margin-left: 8.33333333%;
}
.col-xs-offset-0 {
margin-left: 0%;
}
@media (min-width: $screen-sm) {
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
float: left;
}
.col-sm-12 {
width: 100%;
}
.col-sm-11 {
width: 91.66666667%;
}
.col-sm-10 {
width: 83.33333333%;
}
.col-sm-9 {
width: 75%;
}
.col-sm-8 {
width: 66.66666667%;
}
.col-sm-7 {
width: 58.33333333%;
}
.col-sm-6 {
width: 50%;
}
.col-sm-5 {
width: 41.66666667%;
}
.col-sm-4 {
width: 33.33333333%;
}
.col-sm-3 {
width: 25%;
}
.col-sm-2 {
width: 16.66666667%;
}
.col-sm-1 {
width: 8.33333333%;
}
.col-sm-pull-12 {
right: 100%;
}
.col-sm-pull-11 {
right: 91.66666667%;
}
.col-sm-pull-10 {
right: 83.33333333%;
}
.col-sm-pull-9 {
right: 75%;
}
.col-sm-pull-8 {
right: 66.66666667%;
}
.col-sm-pull-7 {
right: 58.33333333%;
}
.col-sm-pull-6 {
right: 50%;
}
.col-sm-pull-5 {
right: 41.66666667%;
}
.col-sm-pull-4 {
right: 33.33333333%;
}
.col-sm-pull-3 {
right: 25%;
}
.col-sm-pull-2 {
right: 16.66666667%;
}
.col-sm-pull-1 {
right: 8.33333333%;
}
.col-sm-pull-0 {
right: auto;
}
.col-sm-push-12 {
left: 100%;
}
.col-sm-push-11 {
left: 91.66666667%;
}
.col-sm-push-10 {
left: 83.33333333%;
}
.col-sm-push-9 {
left: 75%;
}
.col-sm-push-8 {
left: 66.66666667%;
}
.col-sm-push-7 {
left: 58.33333333%;
}
.col-sm-push-6 {
left: 50%;
}
.col-sm-push-5 {
left: 41.66666667%;
}
.col-sm-push-4 {
left: 33.33333333%;
}
.col-sm-push-3 {
left: 25%;
}
.col-sm-push-2 {
left: 16.66666667%;
}
.col-sm-push-1 {
left: 8.33333333%;
}
.col-sm-push-0 {
left: auto;
}
.col-sm-offset-12 {
margin-left: 100%;
}
.col-sm-offset-11 {
margin-left: 91.66666667%;
}
.col-sm-offset-10 {
margin-left: 83.33333333%;
}
.col-sm-offset-9 {
margin-left: 75%;
}
.col-sm-offset-8 {
margin-left: 66.66666667%;
}
.col-sm-offset-7 {
margin-left: 58.33333333%;
}
.col-sm-offset-6 {
margin-left: 50%;
}
.col-sm-offset-5 {
margin-left: 41.66666667%;
}
.col-sm-offset-4 {
margin-left: 33.33333333%;
}
.col-sm-offset-3 {
margin-left: 25%;
}
.col-sm-offset-2 {
margin-left: 16.66666667%;
}
.col-sm-offset-1 {
margin-left: 8.33333333%;
}
.col-sm-offset-0 {
margin-left: 0%;
}
}
@media (min-width: $screen-md) {
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
float: left;
}
.col-md-12 {
width: 100%;
}
.col-md-11 {
width: 91.66666667%;
}
.col-md-10 {
width: 83.33333333%;
}
.col-md-9 {
width: 75%;
}
.col-md-8 {
width: 66.66666667%;
}
.col-md-7 {
width: 58.33333333%;
}
.col-md-6 {
width: 50%;
}
.col-md-5 {
width: 41.66666667%;
}
.col-md-4 {
width: 33.33333333%;
}
.col-md-3 {
width: 25%;
}
.col-md-2 {
width: 16.66666667%;
}
.col-md-1 {
width: 8.33333333%;
}
.col-md-pull-12 {
right: 100%;
}
.col-md-pull-11 {
right: 91.66666667%;
}
.col-md-pull-10 {
right: 83.33333333%;
}
.col-md-pull-9 {
right: 75%;
}
.col-md-pull-8 {
right: 66.66666667%;
}
.col-md-pull-7 {
right: 58.33333333%;
}
.col-md-pull-6 {
right: 50%;
}
.col-md-pull-5 {
right: 41.66666667%;
}
.col-md-pull-4 {
right: 33.33333333%;
}
.col-md-pull-3 {
right: 25%;
}
.col-md-pull-2 {
right: 16.66666667%;
}
.col-md-pull-1 {
right: 8.33333333%;
}
.col-md-pull-0 {
right: auto;
}
.col-md-push-12 {
left: 100%;
}
.col-md-push-11 {
left: 91.66666667%;
}
.col-md-push-10 {
left: 83.33333333%;
}
.col-md-push-9 {
left: 75%;
}
.col-md-push-8 {
left: 66.66666667%;
}
.col-md-push-7 {
left: 58.33333333%;
}
.col-md-push-6 {
left: 50%;
}
.col-md-push-5 {
left: 41.66666667%;
}
.col-md-push-4 {
left: 33.33333333%;
}
.col-md-push-3 {
left: 25%;
}
.col-md-push-2 {
left: 16.66666667%;
}
.col-md-push-1 {
left: 8.33333333%;
}
.col-md-push-0 {
left: auto;
}
.col-md-offset-12 {
margin-left: 100%;
}
.col-md-offset-11 {
margin-left: 91.66666667%;
}
.col-md-offset-10 {
margin-left: 83.33333333%;
}
.col-md-offset-9 {
margin-left: 75%;
}
.col-md-offset-8 {
margin-left: 66.66666667%;
}
.col-md-offset-7 {
margin-left: 58.33333333%;
}
.col-md-offset-6 {
margin-left: 50%;
}
.col-md-offset-5 {
margin-left: 41.66666667%;
}
.col-md-offset-4 {
margin-left: 33.33333333%;
}
.col-md-offset-3 {
margin-left: 25%;
}
.col-md-offset-2 {
margin-left: 16.66666667%;
}
.col-md-offset-1 {
margin-left: 8.33333333%;
}
.col-md-offset-0 {
margin-left: 0%;
}
}
@media (min-width: $screen-lg) {
.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
float: left;
}
.col-lg-12 {
width: 100%;
}
.col-lg-11 {
width: 91.66666667%;
}
.col-lg-10 {
width: 83.33333333%;
}
.col-lg-9 {
width: 75%;
}
.col-lg-8 {
width: 66.66666667%;
}
.col-lg-7 {
width: 58.33333333%;
}
.col-lg-6 {
width: 50%;
}
.col-lg-5 {
width: 41.66666667%;
}
.col-lg-4 {
width: 33.33333333%;
}
.col-lg-3 {
width: 25%;
}
.col-lg-2 {
width: 16.66666667%;
}
.col-lg-1 {
width: 8.33333333%;
}
.col-lg-pull-12 {
right: 100%;
}
.col-lg-pull-11 {
right: 91.66666667%;
}
.col-lg-pull-10 {
right: 83.33333333%;
}
.col-lg-pull-9 {
right: 75%;
}
.col-lg-pull-8 {
right: 66.66666667%;
}
.col-lg-pull-7 {
right: 58.33333333%;
}
.col-lg-pull-6 {
right: 50%;
}
.col-lg-pull-5 {
right: 41.66666667%;
}
.col-lg-pull-4 {
right: 33.33333333%;
}
.col-lg-pull-3 {
right: 25%;
}
.col-lg-pull-2 {
right: 16.66666667%;
}
.col-lg-pull-1 {
right: 8.33333333%;
}
.col-lg-pull-0 {
right: auto;
}
.col-lg-push-12 {
left: 100%;
}
.col-lg-push-11 {
left: 91.66666667%;
}
.col-lg-push-10 {
left: 83.33333333%;
}
.col-lg-push-9 {
left: 75%;
}
.col-lg-push-8 {
left: 66.66666667%;
}
.col-lg-push-7 {
left: 58.33333333%;
}
.col-lg-push-6 {
left: 50%;
}
.col-lg-push-5 {
left: 41.66666667%;
}
.col-lg-push-4 {
left: 33.33333333%;
}
.col-lg-push-3 {
left: 25%;
}
.col-lg-push-2 {
left: 16.66666667%;
}
.col-lg-push-1 {
left: 8.33333333%;
}
.col-lg-push-0 {
left: auto;
}
.col-lg-offset-12 {
margin-left: 100%;
}
.col-lg-offset-11 {
margin-left: 91.66666667%;
}
.col-lg-offset-10 {
margin-left: 83.33333333%;
}
.col-lg-offset-9 {
margin-left: 75%;
}
.col-lg-offset-8 {
margin-left: 66.66666667%;
}
.col-lg-offset-7 {
margin-left: 58.33333333%;
}
.col-lg-offset-6 {
margin-left: 50%;
}
.col-lg-offset-5 {
margin-left: 41.66666667%;
}
.col-lg-offset-4 {
margin-left: 33.33333333%;
}
.col-lg-offset-3 {
margin-left: 25%;
}
.col-lg-offset-2 {
margin-left: 16.66666667%;
}
.col-lg-offset-1 {
margin-left: 8.33333333%;
}
.col-lg-offset-0 {
margin-left: 0%;
}
}
.clearfix:before,
.clearfix:after,
.container:before,
.container:after,
.container-fluid:before,
.container-fluid:after,
.row:before,
.row:after {
content: " ";
display: table;
}
.clearfix:after,
.container:after,
.container-fluid:after,
.row:after {
clear: both;
}
.center-block {
display: block;
margin-left: auto;
margin-right: auto;
}

View File

@ -2,21 +2,31 @@
* Includes the main navigation header and the faded toolbar. * Includes the main navigation header and the faded toolbar.
*/ */
header .grid {
grid-template-columns: auto min-content auto;
}
@include smaller-than($l) {
header .grid {
grid-template-columns: 1fr;
grid-row-gap: 0;
}
}
header { header {
position: relative;
display: block; display: block;
z-index: 2; z-index: 6;
top: 0; top: 0;
background-color: $primary-dark; background-color: $primary-dark;
color: #fff; color: #fff;
fill: #fff; fill: #fff;
.padded {
padding: $-m;
}
border-bottom: 1px solid #DDD; border-bottom: 1px solid #DDD;
box-shadow: $bs-card;
padding: $-xxs 0;
.links { .links {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-left: $-m;
} }
.links a { .links a {
display: inline-block; display: inline-block;
@ -28,15 +38,6 @@ header {
padding-left: $-m; padding-left: $-m;
padding-right: 0; padding-right: 0;
} }
@include smaller-than($screen-md) {
.links a {
padding-left: $-s;
padding-right: $-s;
}
.dropdown-container {
padding-left: $-s;
}
}
.avatar, .user-name { .avatar, .user-name {
display: inline-block; display: inline-block;
} }
@ -63,27 +64,17 @@ header {
padding-top: 4px; padding-top: 4px;
font-size: 18px; font-size: 18px;
} }
@include smaller-than($screen-md) { @include between($l, $xl) {
padding-left: $-xs; padding-left: $-xs;
.name { .name {
display: none; display: none;
} }
} }
} }
@include smaller-than($screen-sm) {
text-align: center;
.float.right {
float: none;
}
.links a {
padding: $-s;
}
.user-name {
padding-top: $-s;
}
}
} }
.header-search { .header-search {
display: inline-block; display: inline-block;
} }
@ -92,13 +83,16 @@ header .search-box {
margin-top: 10px; margin-top: 10px;
input { input {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 40px;
color: #EEE; color: #EEE;
z-index: 2; z-index: 2;
padding-left: 40px;
} }
button { button {
fill: #EEE; fill: #EEE;
z-index: 1; z-index: 1;
left: 16px;
svg { svg {
margin-right: 0; margin-right: 0;
} }
@ -115,20 +109,11 @@ header .search-box {
:-moz-placeholder { /* Firefox 18- */ :-moz-placeholder { /* Firefox 18- */
color: #DDD; color: #DDD;
} }
@include smaller-than($screen-lg) { @include between($l, $xl) {
max-width: 250px;
}
@include smaller-than($l) {
max-width: 200px; max-width: 200px;
} }
} }
@include smaller-than($s) {
.header-search {
display: block;
}
}
.logo { .logo {
display: inline-block; display: inline-block;
&:hover { &:hover {
@ -151,10 +136,185 @@ header .search-box {
height: 43px; height: 43px;
} }
.breadcrumbs span.sep { .mobile-menu-toggle {
color: #aaa; color: #FFF;
fill: #FFF;
font-size: 2em;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 4px;
padding: 0 $-xs; padding: 0 $-xs;
position: absolute;
right: $-m;
top: 13px;
line-height: 1;
cursor: pointer;
user-select: none;
svg {
margin: 0;
bottom: -2px;
}
} }
@include smaller-than($l) {
header .header-links {
display: none;
background-color: #FFF;
z-index: 10;
right: $-m;
border-radius: 4px;
overflow: hidden;
position: absolute;
box-shadow: $bs-hover;
margin-top: -$-xs;
&.show {
display: block;
}
}
header .links a, header .dropdown-container ul li a {
text-align: left;
display: block;
padding: $-s $-m;
color: $text-dark;
fill: $text-dark;
svg {
margin-right: $-s;
}
&:hover {
background-color: #EEE;
color: #444;
fill: #444;
text-decoration: none;
}
}
header .dropdown-container {
display: block;
padding-left: 0;
}
header .links {
display: block;
}
header .dropdown-container ul {
display: block !important;
position: relative;
background-color: transparent;
border: 0;
padding: 0;
margin: 0;
box-shadow: none;
}
}
.tri-layout-mobile-tabs {
position: sticky;
top: 0;
z-index: 5;
background-color: #FFF;
border-bottom: 1px solid #DDD;
box-shadow: $bs-card;
}
.tri-layout-mobile-tab {
text-align: center;
border-bottom: 3px solid #BBB;
cursor: pointer;
&:first-child {
border-right: 1px solid #DDD;
}
&.active {
border-bottom-color: currentColor;
}
}
.breadcrumbs {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
opacity: 0.7;
.icon-list-item {
width: auto;
padding-top: $-xs;
padding-bottom: $-xs;
}
.separator {
display: inline-block;
fill: #aaa;
font-size: 1.6em;
line-height: 0.8;
margin: -2px 0 0;
}
&:hover {
opacity: 1;
}
}
@include smaller-than($l) {
.breadcrumbs .icon-list-item {
padding: $-xs;
> span + span {
display: none;
}
> span:first-child {
margin-right: 0;
}
}
}
.breadcrumb-listing {
position: relative;
.breadcrumb-listing-toggle {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
&:hover {
border-color: #DDD;
}
}
.svg-icon {
margin-right: 0;
}
}
.breadcrumb-listing-dropdown {
box-shadow: $bs-med;
overflow: hidden;
min-height: 100px;
width: 240px;
display: none;
position: absolute;
z-index: 80;
right: -$-m;
.breadcrumb-listing-search .svg-icon {
position: absolute;
left: $-s;
top: 11px;
fill: #888;
pointer-events: none;
}
.breadcrumb-listing-entity-list {
max-height: 400px;
overflow-y: scroll;
text-align: left;
}
input {
padding-left: $-xl;
border-radius: 0;
border: 0;
border-bottom: 1px solid #DDD;
}
}
@include smaller-than($m) {
.breadcrumb-listing-dropdown {
position: fixed;
right: auto;
left: $-m;
}
.breadcrumb-listing-dropdown .breadcrumb-listing-entity-list {
max-height: 240px;
}
}
.faded { .faded {
a, button, span, span > div { a, button, span, span > div {
color: #666; color: #666;
@ -175,20 +335,9 @@ header .search-box {
padding: $-s; padding: $-s;
} }
.faded-small { .action-buttons .text-button {
color: #000;
fill: #000;
font-size: 0.9em;
background-color: $primary-faded;
}
.toolbar-container {
background-color: #FFF;
}
.breadcrumbs .text-button, .action-buttons .text-button {
display: inline-block; display: inline-block;
padding: $-s; padding: $-xs $-s;
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }
@ -217,28 +366,12 @@ header .search-box {
} }
@include smaller-than($m) { @include smaller-than($m) {
.breadcrumbs .text-button, .action-buttons .text-button { .action-buttons .text-button {
padding: $-xs $-xs; padding: $-xs $-xs;
} }
.action-buttons .dropdown-container:last-child a { .action-buttons .dropdown-container:last-child a {
padding-left: $-xs; padding-left: $-xs;
} }
.breadcrumbs .text-button {
font-size: 0;
}
.breadcrumbs .text-button svg {
font-size: $fs-m;
}
.breadcrumbs a i {
font-size: $fs-m;
padding-right: 0;
}
.breadcrumbs span.sep {
padding: 0 $-xxs;
}
.toolbar .col-xs-1:first-child {
padding-right: 0;
}
} }
.nav-tabs { .nav-tabs {
@ -254,6 +387,3 @@ header .search-box {
} }
} }
} }
.faded-small .nav-tabs a {
padding: $-s $-m;
}

View File

@ -3,27 +3,18 @@
} }
html { html {
background-color: #FFFFFF;
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: scroll;
background-color: #F2F2F2;
&.flexbox { &.flexbox {
overflow-y: hidden; overflow-y: hidden;
} }
&.shaded {
background-color: #F2F2F2;
}
} }
body { body {
font-size: $fs-m; font-size: $fs-m;
line-height: 1.6; line-height: 1.6;
color: #616161; color: #444;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&.shaded {
background-color: #F2F2F2; background-color: #F2F2F2;
}
}
button {
font-size: 100%;
} }

View File

@ -0,0 +1,316 @@
/**
* Generic content container
*/
.container {
max-width: $xxl;
margin-left: auto;
margin-right: auto;
padding-left: $-m;
padding-right: $-m;
&.small {
max-width: 840px;
}
&.very-small {
max-width: 480px;
}
}
/**
* Core grid layout system
*/
.grid {
display: grid;
grid-column-gap: $-l;
grid-row-gap: $-l;
&.half {
grid-template-columns: 1fr 1fr;
}
&.third {
grid-template-columns: 1fr 1fr 1fr;
}
&.left-focus {
grid-template-columns: 2fr 1fr;
}
&.right-focus {
grid-template-columns: 1fr 3fr;
}
&.gap-y-xs {
grid-row-gap: $-xs;
}
&.gap-xl {
grid-column-gap: $-xl;
grid-row-gap: $-xl;
}
&.gap-xxl {
grid-column-gap: $-xxl;
grid-row-gap: $-xxl;
}
&.v-center {
align-items: center;
}
&.no-gap {
grid-row-gap: 0;
grid-column-gap: 0;
}
&.no-row-gap {
grid-row-gap: 0;
}
}
@include smaller-than($m) {
.grid.third {
grid-template-columns: 1fr 1fr;
}
.grid.half:not(.no-break), .grid.left-focus:not(.no-break), .grid.right-focus:not(.no-break) {
grid-template-columns: 1fr;
}
.grid.half.collapse-xs {
grid-template-columns: 1fr 1fr;
}
.grid.gap-xl {
grid-column-gap: $-m;
grid-row-gap: $-m;
}
.grid.right-focus.reverse-collapse > *:nth-child(2) {
order: 0;
}
.grid.right-focus.reverse-collapse > *:nth-child(1) {
order: 1;
}
}
@include smaller-than($s) {
.grid.third {
grid-template-columns: 1fr;
}
}
@include smaller-than($xs) {
.grid.half.collapse-xs {
grid-template-columns: 1fr;
}
}
/**
* Flexbox layout system
*/
body.flexbox {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
min-height: 100%;
max-height: 100%;
overflow: hidden;
#content {
flex: 1;
display: flex;
min-height: 0;
}
}
.flex-fill {
display: flex;
align-items: stretch;
min-height: 0;
max-width: 100%;
position: relative;
}
.flex {
min-height: 0;
flex: 1;
}
/**
* Display and float utilities
*/
.block {
display: block;
position: relative;
}
.inline {
display: inline;
}
.block.inline {
display: inline-block;
}
.hidden {
display: none;
}
.float {
float: left;
&.right {
float: right;
}
}
/**
* Visibility
*/
@each $sizeLetter, $size in $screen-sizes {
@include smaller-than($size) {
.hide-under-#{$sizeLetter} {
display: none !important;
}
}
@include larger-than($size) {
.hide-over-#{$sizeLetter} {
display: none !important;
}
}
}
/**
* Inline content columns
*/
.dual-column-content {
columns: 2;
}
@include smaller-than($m) {
.dual-column-content {
columns: 1;
}
}
/**
* Fixes
*/
.clearfix:before,
.clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
/**
* View Layouts
*/
.tri-layout-container {
display: grid;
margin-left: $-xl;
margin-right: $-xl;
grid-template-columns: 1fr 4fr 1fr;
grid-template-areas: "a b c";
grid-column-gap: $-xxl;
.tri-layout-right {
grid-area: c;
min-width: 0;
}
.tri-layout-left {
grid-area: a;
min-width: 0;
}
.tri-layout-middle {
grid-area: b;
padding-top: $-m;
}
}
@include smaller-than($xxl) {
.tri-layout-container {
grid-template-areas: "c b b"
"a b b";
grid-template-columns: 1fr 3fr;
grid-template-rows: max-content min-content;
padding-right: $-l;
}
}
@include larger-than($xxl) {
.tri-layout-left-contents, .tri-layout-right-contents {
padding: $-m;
position: sticky;
top: $-m;
max-height: 100vh;
min-height: 50vh;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
.tri-layout-middle-contents {
max-width: 940px;
margin: 0 auto;
}
}
@include smaller-than($l) {
.tri-layout-container {
grid-template-areas: none;
grid-template-columns: 1fr;
grid-column-gap: 0;
padding-right: $-xs;
padding-left: $-xs;
.tri-layout-left-contents, .tri-layout-right-contents {
padding-left: $-m;
padding-right: $-m;
}
.tri-layout-right-contents > div, .tri-layout-left-contents > div {
opacity: 0.6;
z-index: 0;
}
.tri-layout-left > *, .tri-layout-right > * {
display: none;
pointer-events: none;
}
.tri-layout-left, .tri-layout-right {
grid-area: none;
grid-column: 1/1;
grid-row: 1;
padding-top: 0 !important;
}
.tri-layout-middle {
grid-area: none;
grid-row: 3;
grid-column: 1/1;
z-index: 1;
overflow: hidden;
transition: transform ease-in-out 240ms;
}
.tri-layout-left {
grid-row: 2;
}
&.show-info {
overflow: hidden;
.tri-layout-middle {
display: none;
}
.tri-layout-right > *, .tri-layout-left > * {
display: block;
pointer-events: auto;
}
}
}
}
@include larger-than($l) {
.tri-layout-mobile-tabs {
display: none;
}
}
@include smaller-than($m) {
.tri-layout-container {
margin-left: 0;
margin-right: 0;
}
}
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms;
&:hover {
opacity: 1;
}
}

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