mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-16 11:19:37 +03:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
2af0021c2b | ||
|
|
0f2eaccb39 | ||
|
|
b251671e3f | ||
|
|
c4eed37d8e | ||
|
|
8b43b91057 | ||
|
|
91fe7f0bee | ||
|
|
5cfb7b8de4 | ||
|
|
6329a1842a | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
393f6047f2 | ||
|
|
d94fc5b694 | ||
|
|
f06770d91d | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
11960d9d3a | ||
|
|
bbd8fff021 | ||
|
|
ec17bd8608 | ||
|
|
0dbb8babee | ||
|
|
b14e9fc619 | ||
|
|
63c6d3478d | ||
|
|
781f0e7887 | ||
|
|
23e014cb25 | ||
|
|
5b64358ef1 | ||
|
|
56df64063d | ||
|
|
3f81eba13b | ||
|
|
7973412c29 | ||
|
|
f83de5f834 | ||
|
|
f2ceba978a | ||
|
|
96c074bb56 | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
f8a299caee | ||
|
|
437dce7756 | ||
|
|
f8ad820281 | ||
|
|
757f16a3b5 | ||
|
|
632ecc668f | ||
|
|
92d393537c | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
43d9d2eba7 | ||
|
|
baa260a03d | ||
|
|
b157a9927a | ||
|
|
b246a67e8a | ||
|
|
2d958e88bf | ||
|
|
07c7d5af17 | ||
|
|
42976ca48c | ||
|
|
d05e85efa9 | ||
|
|
547e117760 | ||
|
|
50a5d3c546 | ||
|
|
3fd82200cc | ||
|
|
7215392784 | ||
|
|
8a9a8dfae5 | ||
|
|
c44314def3 | ||
|
|
8b899a9cf0 | ||
|
|
0ebdfa4825 | ||
|
|
32a06f119b | ||
|
|
ec30864ce5 | ||
|
|
6bc72e157a | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
10418323ef | ||
|
|
a11d5245ec | ||
|
|
565033e0d4 | ||
|
|
c25ef18900 | ||
|
|
b0a63ba0cc | ||
|
|
7b6c88f17c | ||
|
|
361ba8b244 | ||
|
|
9baa96d41c | ||
|
|
e584b4926f | ||
|
|
bcd9c2044e | ||
|
|
d582e76fed | ||
|
|
991dd8a558 | ||
|
|
bc49784797 | ||
|
|
7f99903fdb | ||
|
|
97d011ac8e | ||
|
|
61596a8e21 | ||
|
|
44e337cef6 | ||
|
|
1bec3eaa1e | ||
|
|
5942d796b5 | ||
|
|
eec9c05518 | ||
|
|
246d1621f5 | ||
|
|
6c1e06bf86 | ||
|
|
3c1e165134 | ||
|
|
80d1c594cc | ||
|
|
947db95d16 | ||
|
|
5b9362ab0b | ||
|
|
f602b088ac | ||
|
|
4acf0c4ee0 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
9d3f329bc9 | ||
|
|
bd00a03e7b | ||
|
|
be517de7dc | ||
|
|
23ab1f0c81 | ||
|
|
49621e7b15 | ||
|
|
1ab6f017c9 | ||
|
|
0b364fd72f | ||
|
|
e80ae76856 | ||
|
|
eebad3e2a0 | ||
|
|
db2af47286 | ||
|
|
7ad28aeab4 | ||
|
|
8d80e7311c | ||
|
|
7932069535 | ||
|
|
78564ec61d | ||
|
|
b80184cd93 | ||
|
|
1fa079b466 | ||
|
|
fcfb9470c9 | ||
|
|
c99653f0f2 | ||
|
|
5080b4996e | ||
|
|
1903422113 | ||
|
|
6f9d2939e7 | ||
|
|
3234510d31 | ||
|
|
a40af08018 | ||
|
|
223a6b546c | ||
|
|
0d5da2d9d4 | ||
|
|
37337b8b1d | ||
|
|
ffcc058927 | ||
|
|
3a1cda5802 | ||
|
|
5c1015d6fc | ||
|
|
3385ec3f78 | ||
|
|
1b46c19849 | ||
|
|
75a4fc905b | ||
|
|
05666efda9 | ||
|
|
59367b3417 | ||
|
|
9a31b83b2a | ||
|
|
a81a56706e | ||
|
|
ada7c83e96 | ||
|
|
ea287ebf86 | ||
|
|
043cdeafb3 | ||
|
|
6e03078de3 |
@@ -3,6 +3,10 @@ APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# The below url has to be set if using social auth options
|
||||
# or if you are not using BookStack at the root path of your domain.
|
||||
# APP_URL=http://bookstack.dev
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=database_database
|
||||
@@ -42,8 +46,6 @@ GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
11
.github/ISSUE_TEMPLATE.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
### For Feature Requests
|
||||
Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
PHP Version:
|
||||
|
||||
MySQL Version:
|
||||
|
||||
Expected Behavior:
|
||||
|
||||
Actual Behavior:
|
||||
31
.travis.yml
Normal file
31
.travis.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- mysql-server-5.6
|
||||
- mysql-client-core-5.6
|
||||
- mysql-client-5.6
|
||||
|
||||
before_script:
|
||||
- mysql -u root -e 'create database `bookstack-test`;'
|
||||
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer self-update
|
||||
- composer dump-autoload --no-interaction
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- php artisan clear-compiled -n
|
||||
- php artisan optimize -n
|
||||
- php artisan migrate --force -n --database=mysql_testing
|
||||
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
||||
|
||||
script:
|
||||
- phpunit
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Dan Brown
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string key
|
||||
* @property \User user
|
||||
@@ -28,7 +26,7 @@ class Activity extends Model
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +44,7 @@ class Activity extends Model
|
||||
* @return bool
|
||||
*/
|
||||
public function isSimilarTo($activityB) {
|
||||
return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id];
|
||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
40
app/Book.php
40
app/Book.php
@@ -1,35 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
return '/books/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . $this->slug . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . $this->slug);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany('BookStack\Page');
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
{
|
||||
return $this->hasMany('BookStack\Chapter');
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,25 +5,47 @@ class Chapter extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pages that this chapter contains.
|
||||
* @return mixed
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC');
|
||||
return $this->hasMany(Page::class)->orderBy('priority', 'ASC');
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url of this chapter.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
return '/books/' . $bookSlug. '/chapter/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this chapter's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
51
app/Console/Commands/RegeneratePermissions.php
Normal file
51
app/Console/Commands/RegeneratePermissions.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'permissions:regen';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Regenerate all system permissions';
|
||||
|
||||
/**
|
||||
* The service to handle the permission system.
|
||||
*
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->permissionService->buildJointPermissions();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\Inspire::class,
|
||||
\BookStack\Console\Commands\ResetViews::class,
|
||||
\BookStack\Console\Commands\RegeneratePermissions::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class EmailConfirmation extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'token'];
|
||||
|
||||
/**
|
||||
* Get the user that this confirmation is attached to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
114
app/Entity.php
114
app/Entity.php
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
abstract class Entity extends Ownable
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function activity()
|
||||
{
|
||||
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
|
||||
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,15 +51,24 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function views()
|
||||
{
|
||||
return $this->morphMany('BookStack\View', 'viewable');
|
||||
return $this->morphMany(View::class, 'viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tag models that have been user assigned to this entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
public function restrictions()
|
||||
public function permissions()
|
||||
{
|
||||
return $this->morphMany('BookStack\Restriction', 'restrictable');
|
||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +79,28 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function hasRestriction($role_id, $action)
|
||||
{
|
||||
return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
|
||||
return $this->permissions()->where('role_id', '=', $role_id)
|
||||
->where('action', '=', $action)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this entity has live (active) restrictions in place.
|
||||
* @param $role_id
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasActiveRestriction($role_id, $action)
|
||||
{
|
||||
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity jointPermissions this is connected to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->morphMany(JointPermission::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +111,32 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return static::getClassName() === strtolower($type);
|
||||
return static::getType() === strtolower($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity type.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
return strtolower(static::getClassName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of an entity of the given type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\' . $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,24 +157,31 @@ abstract class Entity extends Ownable
|
||||
* @param string[] array $wheres
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$exactTerms = [];
|
||||
$fuzzyTerms = [];
|
||||
$search = static::newQuery();
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/\s/', $term)) {
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
$term = '"' . $term . '"';
|
||||
$safeTerm = htmlentities($term, ENT_QUOTES);
|
||||
$safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm);
|
||||
if (preg_match('/".*?"/', $safeTerm) || is_numeric($safeTerm)) {
|
||||
$safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
|
||||
$exactTerms[] = '%' . $safeTerm . '%';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
$safeTerm = '' . $safeTerm . '*';
|
||||
if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
|
||||
}
|
||||
if ($term !== '*') $terms[$key] = $term;
|
||||
}
|
||||
$termString = implode(' ', $terms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
$isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
|
||||
|
||||
// Perform fulltext search if relevant terms exist.
|
||||
if ($isFuzzy) {
|
||||
$termString = implode(' ', $fuzzyTerms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
}
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
@@ -131,25 +193,21 @@ abstract class Entity extends Ownable
|
||||
}
|
||||
});
|
||||
}
|
||||
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if (static::isA('page')) {
|
||||
if ($this->isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if (static::isA('chapter')) {
|
||||
} else if ($this->isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy('title_relevance', 'desc');
|
||||
return $search->orderBy($orderBy, 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this item.
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getUrl();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Restriction extends Model
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['role_id', 'action'];
|
||||
@@ -16,6 +13,6 @@ class Restriction extends Model
|
||||
*/
|
||||
public function restrictable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
return $this->morphTo('restrictable');
|
||||
}
|
||||
}
|
||||
@@ -47,19 +47,44 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($e instanceof NotifyException) {
|
||||
\Session::flash('error', $e->message);
|
||||
return response()->redirectTo($e->redirectLocation);
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
session()->flash('error', $this->getOriginalMessage($e));
|
||||
return redirect($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
||||
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
* @param Exception $e
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type) {
|
||||
do {
|
||||
if (is_a($e, $type)) return true;
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original exception message.
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e) {
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
class PrettyException extends \Exception {}
|
||||
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
<?php namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Exceptions\PrettyException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
@@ -36,7 +33,6 @@ class AuthController extends Controller
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
protected $userRepo;
|
||||
@@ -53,6 +49,8 @@ class AuthController extends Controller
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->redirectPath = baseUrl('/');
|
||||
$this->redirectAfterLogout = baseUrl('/login');
|
||||
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -147,7 +145,9 @@ class AuthController extends Controller
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
return redirect()->intended($this->redirectPath());
|
||||
$path = session()->pull('url.intended', '/');
|
||||
$path = baseUrl($path, true);
|
||||
return redirect($path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Request;
|
||||
use Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
@@ -29,4 +31,46 @@ class PasswordController extends Controller
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a reset link to the given user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function sendResetLinkEmail(Request $request)
|
||||
{
|
||||
$this->validate($request, ['email' => 'required|email']);
|
||||
|
||||
$broker = $this->getBroker();
|
||||
|
||||
$response = Password::broker($broker)->sendResetLink(
|
||||
$request->only('email'), $this->resetEmailBuilder()
|
||||
);
|
||||
|
||||
switch ($response) {
|
||||
case Password::RESET_LINK_SENT:
|
||||
$message = 'A password reset link has been sent to ' . $request->get('email') . '.';
|
||||
session()->flash('success', $message);
|
||||
return $this->getSendResetLinkEmailSuccessResponse($response);
|
||||
|
||||
case Password::INVALID_USER:
|
||||
default:
|
||||
return $this->getSendResetLinkEmailFailureResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for after a successful password reset.
|
||||
*
|
||||
* @param string $response
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function getResetSuccessResponse($response)
|
||||
{
|
||||
$message = 'Your password has been successfully reset.';
|
||||
session()->flash('success', $message);
|
||||
return redirect($this->redirectPath())->with('status', trans($response));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use Activity;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
@@ -71,11 +70,7 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->bookRepo->newFromInput($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name);
|
||||
$book->created_by = Auth::user()->id;
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->bookRepo->createFromInput($request->all());
|
||||
Activity::add($book, 'book_create', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -88,6 +83,7 @@ class BookController extends Controller
|
||||
public function show($slug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-view', $book);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
Views::add($book);
|
||||
$this->setPageTitle($book->getShortName());
|
||||
@@ -121,10 +117,7 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book->fill($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->bookRepo->updateFromInput($book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -186,21 +179,31 @@ class BookController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
$sortedBooks = [];
|
||||
// Sort pages and chapters
|
||||
$sortedBooks = [];
|
||||
$updatedModels = collect();
|
||||
$sortMap = json_decode($request->get('sort-tree'));
|
||||
$defaultBookId = $book->id;
|
||||
foreach ($sortMap as $index => $bookChild) {
|
||||
$id = $bookChild->id;
|
||||
|
||||
// Loop through contents of provided map and update entities accordingly
|
||||
foreach ($sortMap as $bookChild) {
|
||||
$priority = $bookChild->sort;
|
||||
$id = intval($bookChild->id);
|
||||
$isPage = $bookChild->type == 'page';
|
||||
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
|
||||
$bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
|
||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||
$model->priority = $index;
|
||||
if ($isPage) {
|
||||
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
|
||||
|
||||
// Update models only if there's a change in parent chain or ordering.
|
||||
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||
$model->priority = $priority;
|
||||
if ($isPage) $model->chapter_id = $chapterId;
|
||||
$model->save();
|
||||
$updatedModels->push($model);
|
||||
}
|
||||
$model->save();
|
||||
|
||||
// Store involved books to be sorted later
|
||||
if (!in_array($bookId, $sortedBooks)) {
|
||||
$sortedBooks[] = $bookId;
|
||||
}
|
||||
@@ -212,6 +215,9 @@ class BookController extends Controller
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
|
||||
// Update permissions on changed models
|
||||
$this->bookRepo->buildJointPermissions($updatedModels);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -226,7 +232,7 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
Activity::addMessage('book_delete', 0, $book->name);
|
||||
Activity::removeEntity($book);
|
||||
$this->bookRepo->destroyBySlug($bookSlug);
|
||||
$this->bookRepo->destroy($book);
|
||||
return redirect('/books');
|
||||
}
|
||||
|
||||
@@ -257,7 +263,7 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
|
||||
$this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
|
||||
session()->flash('success', 'Book Restrictions Updated');
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -57,12 +57,9 @@ class ChapterController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
|
||||
$chapter = $this->chapterRepo->newFromInput($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
|
||||
$chapter->priority = $this->bookRepo->getNewPriority($book);
|
||||
$chapter->created_by = auth()->user()->id;
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$book->chapters()->save($chapter);
|
||||
$input = $request->all();
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($book);
|
||||
$chapter = $this->chapterRepo->createFromInput($input, $book);
|
||||
Activity::add($chapter, 'chapter_create', $book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
@@ -77,6 +74,7 @@ class ChapterController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($chapter);
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
@@ -156,6 +154,63 @@ class ChapterController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for moving a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $chapterSlug) {
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
return view('chapters/move', [
|
||||
'chapter' => $chapter,
|
||||
'book' => $book
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the move action for a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
$parent = false;
|
||||
|
||||
if ($entityType == 'book') {
|
||||
$parent = $this->bookRepo->getById($entityId);
|
||||
}
|
||||
|
||||
if ($parent === false || $parent === null) {
|
||||
session()->flash('The selected Book was not found');
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->chapterRepo->changeBook($parent->id, $chapter, true);
|
||||
Activity::add($chapter, 'chapter_move', $chapter->book->id);
|
||||
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
@@ -186,7 +241,7 @@ class ChapterController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
|
||||
$this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
|
||||
session()->flash('success', 'Chapter Restrictions Updated');
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
@@ -110,4 +110,15 @@ abstract class Controller extends BaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send back a json error message.
|
||||
* @param string $messageText
|
||||
* @param int $statusCode
|
||||
* @return mixed
|
||||
*/
|
||||
protected function jsonError($messageText = "", $statusCode = 500)
|
||||
{
|
||||
return response()->json(['message' => $messageText], $statusCode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ class ImageController extends Controller
|
||||
$this->validate($request, [
|
||||
'term' => 'required|string'
|
||||
]);
|
||||
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class ImageController extends Controller
|
||||
{
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => 'image|mimes:jpeg,gif,png'
|
||||
'file' => 'is_image'
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
|
||||
@@ -69,10 +69,10 @@ class PageController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$draft = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft);
|
||||
$this->checkOwnablePermission('page-create', $book);
|
||||
$this->setPageTitle('Edit Page Draft');
|
||||
|
||||
return view('pages/create', ['draft' => $draft, 'book' => $book]);
|
||||
return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ class PageController extends Controller
|
||||
|
||||
$draftPage = $this->pageRepo->getById($pageId, true);
|
||||
|
||||
$chapterId = $draftPage->chapter_id;
|
||||
$chapterId = intval($draftPage->chapter_id);
|
||||
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
@@ -128,6 +128,8 @@ class PageController extends Controller
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
@@ -219,8 +221,8 @@ class PageController extends Controller
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Draft saved at ',
|
||||
'status' => 'success',
|
||||
'message' => 'Draft saved at ',
|
||||
'timestamp' => $utcUpdateTimestamp
|
||||
]);
|
||||
}
|
||||
@@ -410,7 +412,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyCreated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
|
||||
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Created Pages',
|
||||
'pages' => $pages
|
||||
@@ -423,7 +425,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Updated Pages',
|
||||
'pages' => $pages
|
||||
@@ -449,7 +451,68 @@ class PageController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this page.
|
||||
* Show the view to choose a new parent to move a page into.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
return view('pages/move', [
|
||||
'book' => $book,
|
||||
'page' => $page
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the action of moving the location of a page
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
$parent = false;
|
||||
|
||||
if ($entityType == 'chapter') {
|
||||
$parent = $this->chapterRepo->getById($entityId);
|
||||
} else if ($entityType == 'book') {
|
||||
$parent = $this->bookRepo->getById($entityId);
|
||||
}
|
||||
|
||||
if ($parent === false || $parent === null) {
|
||||
session()->flash('The selected Book or Chapter was not found');
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->pageRepo->changePageParent($page, $parent);
|
||||
Activity::add($page, 'page_move', $page->book->id);
|
||||
session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
|
||||
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permissions for this page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param Request $request
|
||||
@@ -460,8 +523,8 @@ class PageController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->pageRepo->updateRestrictionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Restrictions Updated');
|
||||
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Permissions Updated');
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
@@ -62,11 +63,13 @@ class PermissionController extends Controller
|
||||
* Show the form for editing a user role.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function editRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
if ($role->hidden) throw new PermissionsException('This role cannot be edited');
|
||||
return view('settings/roles/edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Services\ViewService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
@@ -15,18 +15,21 @@ class SearchController extends Controller
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $viewService;
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param $pageRepo
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param ViewService $viewService
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->viewService = $viewService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -48,9 +51,9 @@ class SearchController extends Controller
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle('Search For ' . $searchTerm);
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -69,8 +72,8 @@ class SearchController extends Controller
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Page Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $pages,
|
||||
'title' => 'Page Search Results',
|
||||
'entities' => $pages,
|
||||
'title' => 'Page Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -89,8 +92,8 @@ class SearchController extends Controller
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Chapter Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $chapters,
|
||||
'title' => 'Chapter Search Results',
|
||||
'entities' => $chapters,
|
||||
'title' => 'Chapter Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -109,8 +112,8 @@ class SearchController extends Controller
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
|
||||
$this->setPageTitle('Book Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $books,
|
||||
'title' => 'Book Search Results',
|
||||
'entities' => $books,
|
||||
'title' => 'Book Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -134,4 +137,35 @@ class SearchController extends Controller
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search for a list of entities and return a partial HTML response of matching entities.
|
||||
* Returns the most popular entities if no search is provided.
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entities = collect();
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items());
|
||||
if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items());
|
||||
if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items());
|
||||
$entities = $entities->sortByDesc('title_relevance');
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
})->toArray();
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames);
|
||||
}
|
||||
|
||||
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
75
app/Http/Controllers/TagController.php
Normal file
75
app/Http/Controllers/TagController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\TagRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* TagController constructor.
|
||||
* @param $tagRepo
|
||||
*/
|
||||
public function __construct(TagRepo $tagRepo)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the Tags for a particular entity
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
|
||||
return response()->json($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tags for a particular entity.
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateForEntity($entityType, $entityId, Request $request)
|
||||
{
|
||||
$entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
|
||||
if ($entity === null) return $this->jsonError("Entity not found", 404);
|
||||
|
||||
$inputTags = $request->input('tags');
|
||||
$tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
|
||||
return response()->json([
|
||||
'tags' => $tags,
|
||||
'message' => 'Tags successfully updated'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from a given search term.
|
||||
* @param Request $request
|
||||
*/
|
||||
public function getNameSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from a given search term.
|
||||
* @param Request $request
|
||||
*/
|
||||
public function getValueSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$tagName = $request->has('name') ? $request->get('name') : false;
|
||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,14 +31,21 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Display a listing of the users.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$users = $this->userRepo->getAllUsers();
|
||||
$listDetails = [
|
||||
'order' => $request->has('order') ? $request->get('order') : 'asc',
|
||||
'search' => $request->has('search') ? $request->get('search') : '',
|
||||
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
|
||||
];
|
||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||
$this->setPageTitle('Users');
|
||||
return view('users/index', ['users' => $users]);
|
||||
$users->appends($listDetails);
|
||||
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +56,8 @@ class UserController extends Controller
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
return view('users/create', ['authMethod' => $authMethod]);
|
||||
$roles = $this->userRepo->getAssignableRoles();
|
||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +125,8 @@ class UserController extends Controller
|
||||
$user = $this->user->findOrFail($id);
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
|
||||
$roles = $this->userRepo->getAssignableRoles();
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,11 +207,14 @@ class UserController extends Controller
|
||||
});
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
if ($this->userRepo->isOnlyAdmin($user)) {
|
||||
session()->flash('error', 'You cannot delete the only admin');
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
$this->userRepo->destroy($user);
|
||||
session()->flash('success', 'User successfully removed');
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ class Authenticate
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||
return redirect()->guest('/register/confirm/awaiting');
|
||||
return redirect()->guest(baseUrl('/register/confirm/awaiting'));
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
return redirect()->guest('/login');
|
||||
return redirect()->guest(baseUrl('/login'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,14 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
|
||||
Route::post('/{bookSlug}/page/{pageId}', 'PageController@store');
|
||||
Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
|
||||
Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
|
||||
@@ -53,6 +55,8 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@showMove');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
|
||||
@@ -80,11 +84,21 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::delete('/{imageId}', 'ImageController@destroy');
|
||||
});
|
||||
|
||||
// Ajax routes
|
||||
// AJAX routes
|
||||
Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
|
||||
Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
|
||||
Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
|
||||
|
||||
// Tag routes (AJAX)
|
||||
Route::group(['prefix' => 'ajax/tags'], function() {
|
||||
Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
|
||||
Route::get('/suggest/names', 'TagController@getNameSuggestions');
|
||||
Route::get('/suggest/values', 'TagController@getValueSuggestions');
|
||||
Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
|
||||
});
|
||||
|
||||
Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
|
||||
|
||||
// Links
|
||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||
|
||||
|
||||
24
app/JointPermission.php
Normal file
24
app/JointPermission.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the role that this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphOne(Entity::class, 'entity');
|
||||
}
|
||||
}
|
||||
19
app/Model.php
Normal file
19
app/Model.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
||||
|
||||
class Model extends EloquentModel
|
||||
{
|
||||
|
||||
/**
|
||||
* Provides public access to get the raw attribute value from the model.
|
||||
* Used in areas where no mutations are required but performance is critical.
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRawAttribute($key)
|
||||
{
|
||||
return parent::getAttributeFromArray($key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
@@ -10,7 +9,7 @@ abstract class Ownable extends Model
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,7 +18,7 @@ abstract class Ownable extends Model
|
||||
*/
|
||||
public function updatedBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'updated_by');
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
50
app/Page.php
50
app/Page.php
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
@@ -10,6 +7,10 @@ class Page extends Entity
|
||||
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toSimpleArray()
|
||||
{
|
||||
$array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
|
||||
@@ -17,34 +18,65 @@ class Page extends Entity
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book this page sits in.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function chapter()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Chapter');
|
||||
return $this->belongsTo(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this page has a chapter.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChapter()
|
||||
{
|
||||
return $this->chapter()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated page revisions, ordered by created date.
|
||||
* @return mixed
|
||||
*/
|
||||
public function revisions()
|
||||
{
|
||||
return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url for this page.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
$midText = $this->draft ? '/draft/' : '/page/';
|
||||
$idComponent = $this->draft ? $this->id : $this->slug;
|
||||
return '/books/' . $bookSlug . $midText . $idComponent;
|
||||
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . $bookSlug . $midText . $idComponent . '/' . trim($path, '/'));
|
||||
}
|
||||
|
||||
return baseUrl('/books/' . $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;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown'];
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision
|
||||
@@ -12,7 +11,7 @@ class PageRevision extends Model
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,7 +20,7 @@ class PageRevision extends Model
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Page');
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\User;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -15,7 +11,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
// Custom validation methods
|
||||
\Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
|
||||
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
|
||||
return in_array($value->getMimeType(), $imageMimes);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use BookStack\Services\LdapService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
|
||||
return new LdapUserProvider($config['model'], $app[LdapService::class]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\ViewService;
|
||||
use BookStack\Setting;
|
||||
use BookStack\View;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\Services\ActivityService;
|
||||
use BookStack\Services\SettingService;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
@@ -29,30 +36,30 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
return new ActivityService(
|
||||
$this->app->make('BookStack\Activity'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
$this->app->make(Activity::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
return new ViewService(
|
||||
$this->app->make('BookStack\View'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
$this->app->make(View::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('setting', function() {
|
||||
return new SettingService(
|
||||
$this->app->make('BookStack\Setting'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(Setting::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('images', function() {
|
||||
return new ImageService(
|
||||
$this->app->make('Intervention\Image\ImageManager'),
|
||||
$this->app->make('Illuminate\Contracts\Filesystem\Factory'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(ImageManager::class),
|
||||
$this->app->make(Factory::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
30
app/Providers/PaginationServiceProvider.php
Normal file
30
app/Providers/PaginationServiceProvider.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
|
||||
class PaginationServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Paginator::currentPathResolver(function () {
|
||||
return baseUrl($this->app['request']->path());
|
||||
});
|
||||
|
||||
Paginator::currentPageResolver(function ($pageName = 'page') {
|
||||
$page = $this->app['request']->input($pageName);
|
||||
|
||||
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Alpha\B;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
use Views;
|
||||
@@ -29,7 +31,7 @@ class BookRepo extends EntityRepo
|
||||
*/
|
||||
private function bookQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book, 'view');
|
||||
return $this->permissionService->enforceBookRestrictions($this->book, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,21 +125,43 @@ class BookRepo extends EntityRepo
|
||||
|
||||
/**
|
||||
* Get a new book instance from request input.
|
||||
* @param $input
|
||||
* @param array $input
|
||||
* @return Book
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
public function createFromInput($input)
|
||||
{
|
||||
return $this->book->newInstance($input);
|
||||
$book = $this->book->newInstance($input);
|
||||
$book->slug = $this->findSuitableSlug($book->name);
|
||||
$book->created_by = auth()->user()->id;
|
||||
$book->updated_by = auth()->user()->id;
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a book identified by the given slug.
|
||||
* @param $bookSlug
|
||||
* Update the given book from user input.
|
||||
* @param Book $book
|
||||
* @param $input
|
||||
* @return Book
|
||||
*/
|
||||
public function destroyBySlug($bookSlug)
|
||||
public function updateFromInput(Book $book, $input)
|
||||
{
|
||||
$book->fill($input);
|
||||
$book->slug = $this->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = auth()->user()->id;
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the given book.
|
||||
* @param Book $book
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy(Book $book)
|
||||
{
|
||||
$book = $this->getBySlug($bookSlug);
|
||||
foreach ($book->pages as $page) {
|
||||
$this->pageRepo->destroy($page);
|
||||
}
|
||||
@@ -145,7 +169,8 @@ class BookRepo extends EntityRepo
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->restrictions()->delete();
|
||||
$book->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
@@ -183,12 +208,10 @@ class BookRepo extends EntityRepo
|
||||
*/
|
||||
public function findSuitableSlug($name, $currentId = false)
|
||||
{
|
||||
$originalSlug = Str::slug($name);
|
||||
$slug = $originalSlug;
|
||||
$count = 2;
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
while ($this->doesSlugExist($slug, $currentId)) {
|
||||
$slug = $originalSlug . '-' . $count;
|
||||
$count++;
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
@@ -196,7 +219,7 @@ class BookRepo extends EntityRepo
|
||||
/**
|
||||
* Get all child objects of a book.
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
* Loads the bookslug 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 Book $book
|
||||
* @param bool $filterDrafts
|
||||
* @return mixed
|
||||
@@ -204,7 +227,7 @@ class BookRepo extends EntityRepo
|
||||
public function getChildren(Book $book, $filterDrafts = false)
|
||||
{
|
||||
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
|
||||
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
$pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
|
||||
if ($filterDrafts) {
|
||||
$pageQuery = $pageQuery->where('draft', '=', false);
|
||||
@@ -212,13 +235,16 @@ class BookRepo extends EntityRepo
|
||||
|
||||
$pages = $pageQuery->get();
|
||||
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) {
|
||||
$this->restrictionService->enforcePageRestrictions($query, 'view');
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
|
||||
$this->permissionService->enforcePageRestrictions($query, 'view');
|
||||
if ($filterDrafts) $query->where('draft', '=', false);
|
||||
}]);
|
||||
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapters = $chapterQuery->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$children = $pages->values();
|
||||
foreach ($chapters as $chapter) {
|
||||
$children->push($chapter);
|
||||
}
|
||||
$bookSlug = $book->slug;
|
||||
|
||||
$children->each(function ($child) use ($bookSlug) {
|
||||
@@ -227,7 +253,7 @@ class BookRepo extends EntityRepo
|
||||
$child->pages->each(function ($page) use ($bookSlug) {
|
||||
$page->setAttribute('bookSlug', $bookSlug);
|
||||
});
|
||||
$child->pages = $child->pages->sortBy(function($child, $key) {
|
||||
$child->pages = $child->pages->sortBy(function ($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->draft) $score -= 100;
|
||||
return $score;
|
||||
@@ -236,7 +262,7 @@ class BookRepo extends EntityRepo
|
||||
});
|
||||
|
||||
// Sort items with drafts first then by priority.
|
||||
return $children->sortBy(function($child, $key) {
|
||||
return $children->sortBy(function ($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->isA('page') && $child->draft) $score -= 100;
|
||||
return $score;
|
||||
@@ -253,8 +279,9 @@ class BookRepo extends EntityRepo
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
|
||||
$bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
|
||||
$books = $bookQuery->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
|
||||
@@ -2,19 +2,32 @@
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
||||
class ChapterRepo extends EntityRepo
|
||||
{
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* Base query for getting chapters, Takes restrictions into account.
|
||||
* ChapterRepo constructor.
|
||||
* @param $pageRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting chapters, Takes permissions into account.
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,9 +79,9 @@ class ChapterRepo extends EntityRepo
|
||||
*/
|
||||
public function getChildren(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
|
||||
$pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
|
||||
// Sort items with drafts first then by priority.
|
||||
return $pages->sortBy(function($child, $key) {
|
||||
return $pages->sortBy(function ($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->draft) $score -= 100;
|
||||
return $score;
|
||||
@@ -78,11 +91,18 @@ class ChapterRepo extends EntityRepo
|
||||
/**
|
||||
* Create a new chapter from request input.
|
||||
* @param $input
|
||||
* @return $this
|
||||
* @param Book $book
|
||||
* @return Chapter
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
public function createFromInput($input, Book $book)
|
||||
{
|
||||
return $this->chapter->fill($input);
|
||||
$chapter = $this->chapter->newInstance($input);
|
||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
|
||||
$chapter->created_by = auth()->user()->id;
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$chapter = $book->chapters()->save($chapter);
|
||||
$this->permissionService->buildJointPermissionsForEntity($chapter);
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +119,8 @@ class ChapterRepo extends EntityRepo
|
||||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->restrictions()->delete();
|
||||
$chapter->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
@@ -130,6 +151,7 @@ class ChapterRepo extends EntityRepo
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
@@ -159,8 +181,9 @@ class ChapterRepo extends EntityRepo
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
|
||||
$chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
|
||||
$chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
@@ -172,19 +195,32 @@ class ChapterRepo extends EntityRepo
|
||||
|
||||
/**
|
||||
* Changes the book relation of this chapter.
|
||||
* @param $bookId
|
||||
* @param $bookId
|
||||
* @param Chapter $chapter
|
||||
* @param bool $rebuildPermissions
|
||||
* @return Chapter
|
||||
*/
|
||||
public function changeBook($bookId, Chapter $chapter)
|
||||
public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
|
||||
{
|
||||
$chapter->book_id = $bookId;
|
||||
// Update related activity
|
||||
foreach ($chapter->activity as $activity) {
|
||||
$activity->book_id = $bookId;
|
||||
$activity->save();
|
||||
}
|
||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
|
||||
$chapter->save();
|
||||
// Update all child pages
|
||||
foreach ($chapter->pages as $page) {
|
||||
$this->pageRepo->changeBook($bookId, $page);
|
||||
}
|
||||
|
||||
// Update permissions if applicable
|
||||
if ($rebuildPermissions) {
|
||||
$chapter->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
@@ -26,9 +28,15 @@ class EntityRepo
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var RestrictionService
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
@@ -38,7 +46,7 @@ class EntityRepo
|
||||
$this->book = app(Book::class);
|
||||
$this->chapter = app(Chapter::class);
|
||||
$this->page = app(Page::class);
|
||||
$this->restrictionService = app(RestrictionService::class);
|
||||
$this->permissionService = app(PermissionService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +58,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforceBookRestrictions($this->book)
|
||||
$query = $this->permissionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
@@ -66,7 +74,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book)
|
||||
return $this->permissionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
@@ -79,7 +87,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforcePageRestrictions($this->page)
|
||||
$query = $this->permissionService->enforcePageRestrictions($this->page)
|
||||
->orderBy('created_at', 'desc')->where('draft', '=', false);
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
@@ -96,7 +104,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforceChapterRestrictions($this->chapter)
|
||||
$query = $this->permissionService->enforceChapterRestrictions($this->chapter)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
@@ -112,7 +120,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->restrictionService->enforcePageRestrictions($this->page)
|
||||
return $this->permissionService->enforcePageRestrictions($this->page)
|
||||
->where('draft', '=', false)
|
||||
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
@@ -136,14 +144,14 @@ class EntityRepo
|
||||
* @param $request
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function updateRestrictionsFromRequest($request, Entity $entity)
|
||||
public function updateEntityPermissionsFromRequest($request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
|
||||
$entity->restrictions()->delete();
|
||||
$entity->permissions()->delete();
|
||||
if ($request->has('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->restrictions()->create([
|
||||
$entity->permissions()->create([
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action)
|
||||
]);
|
||||
@@ -151,6 +159,7 @@ class EntityRepo
|
||||
}
|
||||
}
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,18 +169,117 @@ class EntityRepo
|
||||
* @param $termString
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareSearchTerms($termString)
|
||||
public function prepareSearchTerms($termString)
|
||||
{
|
||||
preg_match_all('/"(.*?)"/', $termString, $matches);
|
||||
$termString = $this->cleanSearchTermString($termString);
|
||||
preg_match_all('/(".*?")/', $termString, $matches);
|
||||
$terms = [];
|
||||
if (count($matches[1]) > 0) {
|
||||
$terms = $matches[1];
|
||||
foreach ($matches[1] as $match) {
|
||||
$terms[] = $match;
|
||||
}
|
||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||
} else {
|
||||
$terms = [];
|
||||
}
|
||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any special search notation that should not
|
||||
* be used in a full-text search.
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function cleanSearchTermString($termString)
|
||||
{
|
||||
// Strip tag searches
|
||||
$termString = preg_replace('/\[.*?\]/', '', $termString);
|
||||
// Reduced multiple spacing into single spacing
|
||||
$termString = preg_replace("/\s{2,}/", " ", $termString);
|
||||
return $termString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses advanced search notations and adds them to the db query.
|
||||
* @param $query
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function addAdvancedSearchQueries($query, $termString)
|
||||
{
|
||||
$escapedOperators = $this->getRegexEscapedOperators();
|
||||
// Look for tag searches
|
||||
preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
|
||||
if (count($tags[0]) > 0) {
|
||||
$this->applyTagSearches($query, $tags);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply extracted tag search terms onto a entity query.
|
||||
* @param $query
|
||||
* @param $tags
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearches($query, $tags) {
|
||||
$query->where(function($query) use ($tags) {
|
||||
foreach ($tags[1] as $index => $tagName) {
|
||||
$query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
|
||||
$tagOperator = $tags[3][$index];
|
||||
$tagValue = $tags[4][$index];
|
||||
if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias method to update the book jointPermissions in the PermissionService.
|
||||
* @param Collection $collection collection on entities
|
||||
*/
|
||||
public function buildJointPermissions(Collection $collection)
|
||||
{
|
||||
$this->permissionService->buildJointPermissionsForEntities($collection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
@@ -13,21 +13,21 @@ class ImageRepo
|
||||
|
||||
protected $image;
|
||||
protected $imageService;
|
||||
protected $restictionService;
|
||||
protected $restrictionService;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
* @param Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
|
||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
$this->restictionService = $restrictionService;
|
||||
$this->restrictionService = $permissionService;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class ImageRepo
|
||||
*/
|
||||
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||
$hasMore = count($images) > $pageSize;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
@@ -14,14 +15,17 @@ class PageRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $pageRevision;
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
* @param PageRevision $pageRevision
|
||||
* @param TagRepo $tagRepo
|
||||
*/
|
||||
public function __construct(PageRevision $pageRevision)
|
||||
public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
|
||||
{
|
||||
$this->pageRevision = $pageRevision;
|
||||
$this->tagRepo = $tagRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -32,7 +36,7 @@ class PageRepo extends EntityRepo
|
||||
*/
|
||||
private function pageQuery($allowDrafts = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforcePageRestrictions($this->page, 'view');
|
||||
$query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
|
||||
if (!$allowDrafts) {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
@@ -76,7 +80,7 @@ class PageRepo extends EntityRepo
|
||||
{
|
||||
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function ($query) {
|
||||
$this->restrictionService->enforcePageRestrictions($query);
|
||||
$this->permissionService->enforcePageRestrictions($query);
|
||||
})
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
|
||||
@@ -142,12 +146,19 @@ class PageRepo extends EntityRepo
|
||||
{
|
||||
$draftPage->fill($input);
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
|
||||
}
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
|
||||
$draftPage->html = $this->formatHtml($input['html']);
|
||||
$draftPage->text = strip_tags($draftPage->html);
|
||||
$draftPage->draft = false;
|
||||
|
||||
$draftPage->save();
|
||||
$this->saveRevision($draftPage, 'Initial Publish');
|
||||
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
@@ -168,6 +179,7 @@ class PageRepo extends EntityRepo
|
||||
if ($chapter) $page->chapter_id = $chapter->id;
|
||||
|
||||
$book->pages()->save($page);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -241,8 +253,9 @@ class PageRepo extends EntityRepo
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
|
||||
$pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
|
||||
$pages = $pageQuery->paginate($count)->appends($paginationAppends);
|
||||
|
||||
// Add highlights to page text.
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
@@ -297,16 +310,20 @@ class PageRepo extends EntityRepo
|
||||
*/
|
||||
public function updatePage(Page $page, $book_id, $input)
|
||||
{
|
||||
// Save a revision before updating
|
||||
if ($page->html !== $input['html'] || $page->name !== $input['name']) {
|
||||
$this->saveRevision($page);
|
||||
}
|
||||
// Hold the old details to compare later
|
||||
$oldHtml = $page->html;
|
||||
$oldName = $page->name;
|
||||
|
||||
// Prevent slug being updated if no name change
|
||||
if ($page->name !== $input['name']) {
|
||||
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
|
||||
}
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$userId = auth()->user()->id;
|
||||
$page->fill($input);
|
||||
@@ -319,6 +336,11 @@ class PageRepo extends EntityRepo
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->userUpdateDraftsQuery($page, $userId)->delete();
|
||||
|
||||
// Save a revision after updating
|
||||
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
|
||||
$this->saveRevision($page, $input['summary']);
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -344,9 +366,10 @@ class PageRepo extends EntityRepo
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
* @param null|string $summary
|
||||
* @return $this
|
||||
*/
|
||||
public function saveRevision(Page $page)
|
||||
public function saveRevision(Page $page, $summary = null)
|
||||
{
|
||||
$revision = $this->pageRevision->fill($page->toArray());
|
||||
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
||||
@@ -356,6 +379,7 @@ class PageRepo extends EntityRepo
|
||||
$revision->created_by = auth()->user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
$revision->save();
|
||||
// Clear old revisions
|
||||
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
||||
@@ -389,7 +413,7 @@ class PageRepo extends EntityRepo
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
}
|
||||
@@ -557,16 +581,33 @@ class PageRepo extends EntityRepo
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
* @param Page $page
|
||||
* @param Entity $parent
|
||||
*/
|
||||
public function changePageParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = $parent->isA('book') ? $parent : $parent->book;
|
||||
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
|
||||
$page->save();
|
||||
$page = $this->changeBook($book->id, $page);
|
||||
$page->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a suitable slug for the resource
|
||||
* @param $name
|
||||
* @param $bookId
|
||||
* @param string $name
|
||||
* @param int $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
@@ -577,12 +618,14 @@ class PageRepo extends EntityRepo
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param $page
|
||||
*/
|
||||
public function destroy($page)
|
||||
public function destroy(Page $page)
|
||||
{
|
||||
Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->tags()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->restrictions()->delete();
|
||||
$page->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Permission;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
@@ -11,16 +12,21 @@ class PermissionsRepo
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
|
||||
protected $systemRoles = ['admin', 'public'];
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param $permission
|
||||
* @param $role
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Permission $permission, Role $role)
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +35,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->all();
|
||||
return $this->role->where('hidden', '=', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +45,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role)
|
||||
{
|
||||
return $this->role->where('id', '!=', $role->id)->get();
|
||||
return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +75,7 @@ class PermissionsRepo
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
return $role;
|
||||
}
|
||||
|
||||
@@ -77,10 +84,14 @@ class PermissionsRepo
|
||||
* Ensure Admin role always has all permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function updateRole($roleId, $roleData)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
@@ -91,6 +102,7 @@ class PermissionsRepo
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,8 +134,8 @@ class PermissionsRepo
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->name === 'admin') {
|
||||
throw new PermissionsException('The admin role cannot be deleted');
|
||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||
throw new PermissionsException('This role is a system role and cannot be deleted');
|
||||
} else if ($role->id == setting('registration-role')) {
|
||||
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
|
||||
}
|
||||
@@ -136,6 +148,7 @@ class PermissionsRepo
|
||||
}
|
||||
}
|
||||
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
|
||||
135
app/Repos/TagRepo.php
Normal file
135
app/Repos/TagRepo.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Tag;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Services\PermissionService;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class TagRepo
|
||||
{
|
||||
|
||||
protected $tag;
|
||||
protected $entity;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* TagRepo constructor.
|
||||
* @param Tag $attr
|
||||
* @param Entity $ent
|
||||
* @param PermissionService $ps
|
||||
*/
|
||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
||||
{
|
||||
$this->tag = $attr;
|
||||
$this->entity = $ent;
|
||||
$this->permissionService = $ps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance of its particular type.
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param string $action
|
||||
*/
|
||||
public function getEntity($entityType, $entityId, $action = 'view')
|
||||
{
|
||||
$entityInstance = $this->entity->getEntityInstance($entityType);
|
||||
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
|
||||
$searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
|
||||
return $searchQuery->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a particular entity.
|
||||
* @param string $entityType
|
||||
* @param int $entityId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$entity = $this->getEntity($entityType, $entityId);
|
||||
if ($entity === null) return collect();
|
||||
|
||||
return $entity->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from scanning existing tag names.
|
||||
* If no search term is given the 50 most popular tag names are provided.
|
||||
* @param $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function getNameSuggestions($searchTerm = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['name'])->pluck('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from scanning existing tag values.
|
||||
* If no search is given the 50 most popular values are provided.
|
||||
* Passing a tagName will only find values for a tags with a particular name.
|
||||
* @param $searchTerm
|
||||
* @param $tagName
|
||||
* @return array
|
||||
*/
|
||||
public function getValueSuggestions($searchTerm = false, $tagName = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['value'])->pluck('value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* @param Entity $entity
|
||||
* @param array $tags
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function saveTagsToEntity(Entity $entity, $tags = [])
|
||||
{
|
||||
$entity->tags()->delete();
|
||||
$newTags = [];
|
||||
foreach ($tags as $tag) {
|
||||
if (trim($tag['name']) === '') continue;
|
||||
$newTags[] = $this->newInstanceFromInput($tag);
|
||||
}
|
||||
|
||||
return $entity->tags()->saveMany($newTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Tag instance from user input.
|
||||
* @param $input
|
||||
* @return static
|
||||
*/
|
||||
protected function newInstanceFromInput($input)
|
||||
{
|
||||
$name = trim($input['name']);
|
||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||
// Any other modification or cleanup required can go here
|
||||
$values = ['name' => $name, 'value' => $value];
|
||||
return $this->tag->newInstance($values);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,27 @@ class UserRepo
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* @param int $count
|
||||
* @param $sortData
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
|
||||
{
|
||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
@@ -168,6 +189,15 @@ class UserRepo
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAssignableRoles()
|
||||
{
|
||||
return $this->role->visible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles which can be given restricted access to
|
||||
* other entities in the system.
|
||||
@@ -175,7 +205,7 @@ class UserRepo
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('name', '!=', 'admin')->get();
|
||||
return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
|
||||
}
|
||||
|
||||
}
|
||||
59
app/Role.php
59
app/Role.php
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
@@ -14,40 +11,54 @@ class Role extends Model
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\User');
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The permissions that belong to the role.
|
||||
* Get all related JointPermissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->hasMany(JointPermission::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The RolePermissions that belong to the role.
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permission');
|
||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role has a permission.
|
||||
* @param $permission
|
||||
* @param $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permission)
|
||||
public function hasPermission($permissionName)
|
||||
{
|
||||
return $this->permissions->pluck('name')->contains($permission);
|
||||
$permissions = $this->getRelationValue('permissions');
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->getRawAttribute('name') === $permissionName) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param Permission $permission
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function attachPermission(Permission $permission)
|
||||
public function attachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->attach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a single permission from this role.
|
||||
* @param Permission $permission
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function detachPermission(Permission $permission)
|
||||
public function detachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
@@ -61,4 +72,24 @@ class Role extends Model
|
||||
{
|
||||
return static::where('name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified system role.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getSystemRole($roleName)
|
||||
{
|
||||
return static::where('system_name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* @return mixed
|
||||
*/
|
||||
public static function visible()
|
||||
{
|
||||
return static::where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
* @param $roleName
|
||||
* @param $name
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
@@ -8,17 +8,17 @@ class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param Activity $activity
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity, RestrictionService $restrictionService)
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ class ActivityService
|
||||
*/
|
||||
public function latest($count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
@@ -105,8 +105,16 @@ class ActivityService
|
||||
*/
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
if ($entity->isA('book')) {
|
||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
||||
} else {
|
||||
$query = $this->activity->where('entity_type', '=', get_class($entity))
|
||||
->where('entity_id', '=', $entity->id);
|
||||
}
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activity);
|
||||
}
|
||||
@@ -121,7 +129,7 @@ class ActivityService
|
||||
*/
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
|
||||
return $this->filterSimilar($activityList);
|
||||
|
||||
@@ -48,11 +48,13 @@ class ExportService
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$pathString = public_path($srcString);
|
||||
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
||||
if ($isLocal) {
|
||||
$pathString = public_path(trim($srcString, '/'));
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
if ($isLocal && !file_exists($pathString)) continue;
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
|
||||
@@ -95,6 +95,7 @@ class ImageService
|
||||
|
||||
try {
|
||||
$storage->put($fullPath, $imageData);
|
||||
$storage->setVisibility($fullPath, 'public');
|
||||
} catch (Exception $e) {
|
||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||
}
|
||||
@@ -167,6 +168,7 @@ class ImageService
|
||||
|
||||
$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);
|
||||
@@ -257,16 +259,22 @@ class ImageService
|
||||
$storageUrl = config('filesystems.url');
|
||||
|
||||
// Get the standard public s3 url if s3 is set as storage type
|
||||
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
|
||||
// region-based url will be used to prevent http issues.
|
||||
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
||||
$storageDetails = config('filesystems.disks.s3');
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
if (strpos($storageDetails['bucket'], '.') === false) {
|
||||
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
|
||||
} else {
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->storageUrl = $storageUrl;
|
||||
}
|
||||
|
||||
return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@ class Ldap
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version number for the given ldap connection.
|
||||
* @param $ldapConnection
|
||||
* @param $version
|
||||
* @return bool
|
||||
*/
|
||||
public function setVersion($ldapConnection, $version)
|
||||
{
|
||||
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search LDAP tree using the provided filter.
|
||||
* @param resource $ldapConnection
|
||||
|
||||
@@ -122,7 +122,7 @@ class LdapService
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
|
||||
581
app/Services/PermissionService.php
Normal file
581
app/Services/PermissionService.php
Normal file
@@ -0,0 +1,581 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\JointPermission;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Page;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
|
||||
protected $userRoles;
|
||||
protected $isAdmin;
|
||||
protected $currentAction;
|
||||
protected $currentUser;
|
||||
|
||||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
|
||||
protected $jointPermission;
|
||||
protected $role;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
{
|
||||
$this->currentUser = auth()->user();
|
||||
$userSet = $this->currentUser !== null;
|
||||
$this->userRoles = false;
|
||||
$this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
|
||||
if (!$userSet) $this->currentUser = new User();
|
||||
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->role = $role;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
*/
|
||||
protected function readyEntityCache()
|
||||
{
|
||||
$this->entityCache = [
|
||||
'books' => collect(),
|
||||
'chapters' => collect()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache
|
||||
* @param $bookId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
{
|
||||
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
|
||||
return $this->entityCache['books']->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->book->find($bookId);
|
||||
if ($book === null) $book = false;
|
||||
if (isset($this->entityCache['books'])) {
|
||||
$this->entityCache['books']->put($bookId, $book);
|
||||
}
|
||||
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
{
|
||||
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
|
||||
return $this->entityCache['chapters']->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->chapter->find($chapterId);
|
||||
if ($chapter === null) $chapter = false;
|
||||
if (isset($this->entityCache['chapters'])) {
|
||||
$this->entityCache['chapters']->put($chapterId, $chapter);
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current user;
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getRoles()
|
||||
{
|
||||
if ($this->userRoles !== false) return $this->userRoles;
|
||||
|
||||
$roles = [];
|
||||
|
||||
if (auth()->guest()) {
|
||||
$roles[] = $this->role->getSystemRole('public')->id;
|
||||
return $roles;
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->currentUser->roles as $role) {
|
||||
$roles[] = $role->id;
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-generate all entity permission from scratch.
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
$this->jointPermission->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
|
||||
$this->createManyJointPermissions($chapters, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
|
||||
$this->createManyJointPermissions($pages, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$roles = $this->role->with('jointPermissions')->get();
|
||||
$entities = collect([$entity]);
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
$entities = $entities->merge($entity->chapters);
|
||||
$entities = $entities->merge($entity->pages);
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
$entities = $entities->merge($entity->pages);
|
||||
}
|
||||
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
$roles = $this->role->with('jointPermissions')->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
$roles = collect([$role]);
|
||||
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForRoles([$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
foreach ($roles as $role) {
|
||||
$role->jointPermissions()->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForEntities([$entity]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
{
|
||||
$query = $this->jointPermission->newQuery();
|
||||
foreach ($entities as $entity) {
|
||||
$query->orWhere(function($query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$query->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param Collection $roles
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
{
|
||||
$this->readyEntityCache();
|
||||
$jointPermissions = [];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($roles as $role) {
|
||||
foreach ($this->getActions($entity) as $action) {
|
||||
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->jointPermission->insert($jointPermissions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions($entity)
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
$baseActions[] = 'page-create';
|
||||
} else if ($entity->isA('book')) {
|
||||
$baseActions[] = 'page-create';
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create entity permission data for an entity and role
|
||||
* for a particular action.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action)
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
|
||||
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
|
||||
$explodedAction = explode('-', $action);
|
||||
$restrictionAction = end($explodedAction);
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
|
||||
if (!$entity->restricted) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
} else {
|
||||
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
|
||||
if (!$entity->restricted) {
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
|
||||
$hasPermissiveAccessToBook = !$book->restricted;
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
|
||||
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
|
||||
} else {
|
||||
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
} elseif ($entity->isA('page')) {
|
||||
|
||||
if (!$entity->restricted) {
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
|
||||
$hasPermissiveAccessToBook = !$book->restricted;
|
||||
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
|
||||
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
|
||||
$acknowledgeChapter = ($chapter && $chapter->restricted);
|
||||
|
||||
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
|
||||
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
} else {
|
||||
$hasAccess = $entity->hasRestriction($role->id, $action);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
* @param $permissionOwn
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
{
|
||||
$entityClass = get_class($entity);
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entityClass,
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'created_by' => $entity->getRawAttribute('created_by')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Ownable $ownable
|
||||
* @param $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
{
|
||||
if ($this->isAdmin) return true;
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
|
||||
$nonJointPermissions = ['restrictions'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
$allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
|
||||
$ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
|
||||
$this->currentAction = 'view';
|
||||
$isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
|
||||
return ($allPermission || ($isOwner && $ownPermission));
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$this->currentAction = $permission;
|
||||
}
|
||||
|
||||
|
||||
return $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The general query filter to remove all entities
|
||||
* that the current user does not have access to.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
protected function entityRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentQuery) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
->where('action', '=', $this->currentAction)
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a page query
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforcePageRestrictions($query, $action = 'view')
|
||||
{
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false);
|
||||
if ($this->currentUser) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return $this->enforceEntityRestrictions($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add on permission restrictions to a chapter query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceChapterRestrictions($query, $action = 'view')
|
||||
{
|
||||
return $this->enforceEntityRestrictions($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions to a book query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceBookRestrictions($query, $action = 'view')
|
||||
{
|
||||
return $this->enforceEntityRestrictions($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceEntityRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
||||
return $query->where(function ($query) use ($tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
return $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', 'Bookstack\\Page')
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Entity;
|
||||
|
||||
class RestrictionService
|
||||
{
|
||||
|
||||
protected $userRoles;
|
||||
protected $isAdmin;
|
||||
protected $currentAction;
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* RestrictionService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->currentUser = auth()->user();
|
||||
$this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
|
||||
$this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function checkIfEntityRestricted(Entity $entity, $action)
|
||||
{
|
||||
if ($this->isAdmin) return true;
|
||||
$this->currentAction = $action;
|
||||
$baseQuery = $entity->where('id', '=', $entity->id);
|
||||
if ($entity->isA('page')) {
|
||||
return $this->pageRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $this->bookRestrictionQuery($baseQuery)->count() > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a page query
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforcePageRestrictions($query, $action = 'view')
|
||||
{
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false);
|
||||
if ($this->currentUser) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->pageRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting pages.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function pageRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where(function ($query) {
|
||||
$query->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', false);
|
||||
})->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
})->where('restricted', '=', false);
|
||||
})->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)->where('chapter_id', '=', 0)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has no chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id');
|
||||
}, 'and', true)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has a chapter with accepted permissions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', true)
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'pages', 'Page');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add on permission restrictions to a chapter query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceChapterRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->chapterRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting chapters.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// Book & chapter unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where('restricted', '=', false)->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
})
|
||||
// Chapter unrestricted & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Chapter has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions to a book query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceBookRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->bookRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting books.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
$parentWhereQuery
|
||||
->where('restricted', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', true)->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
return $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
|
||||
->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->bookRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->chapterRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
return $query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The query to check the restrictions on an entity.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $modelName
|
||||
*/
|
||||
private function checkRestrictionsQuery($query, $tableName, $modelName)
|
||||
{
|
||||
$query->select('*')->from('restrictions')
|
||||
->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
|
||||
->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
|
||||
->where('restrictions.action', '=', $this->currentAction)
|
||||
->whereIn('restrictions.role_id', $this->userRoles);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Http\Controllers\Auth\AuthController;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
@@ -116,20 +113,20 @@ class SocialAuthService
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
\Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
|
||||
session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||
\Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
|
||||
session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
// Change the user that the social account is assigned to.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
@@ -138,6 +135,7 @@ class SocialAuthService
|
||||
if (setting('registration-enabled')) {
|
||||
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
}
|
||||
|
||||
@@ -160,7 +158,7 @@ class SocialAuthService
|
||||
$driver = trim(strtolower($socialDriver));
|
||||
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
|
||||
|
||||
return $driver;
|
||||
}
|
||||
@@ -217,7 +215,7 @@ class SocialAuthService
|
||||
{
|
||||
session();
|
||||
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
\Session::flash('success', $socialDriver . ' account successfully detached');
|
||||
session()->flash('success', title_case($socialDriver) . ' account successfully detached');
|
||||
return redirect(auth()->user()->getEditUrl());
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,18 @@ class ViewService
|
||||
|
||||
protected $view;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(View $view, RestrictionService $restrictionService)
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,17 +50,21 @@ class ViewService
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false $filterModel
|
||||
* @param bool|false|array $filterModel
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
if ($filterModel && is_array($filterModel)) {
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
};
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
@@ -76,7 +80,7 @@ class ViewService
|
||||
{
|
||||
if ($this->user === null) return collect();
|
||||
|
||||
$query = $this->restrictionService
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
@@ -11,6 +8,6 @@ class SocialAccount extends Model
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Tag.php
Normal file
19
app/Tag.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
/**
|
||||
* Class Attribute
|
||||
* @package BookStack
|
||||
*/
|
||||
class Tag extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'value', 'order'];
|
||||
|
||||
/**
|
||||
* Get the entity that this tag belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
}
|
||||
26
app/User.php
26
app/User.php
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
@@ -52,7 +49,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +113,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function socialAccounts()
|
||||
{
|
||||
return $this->hasMany('BookStack\SocialAccount');
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,8 +138,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
{
|
||||
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return '/user_avatar.png';
|
||||
return $this->avatar->getThumb($size, $size, false);
|
||||
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return baseUrl('/user_avatar.png');
|
||||
return baseUrl($this->avatar->getThumb($size, $size, false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +148,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Image', 'image_id');
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +157,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return '/settings/users/' . $this->id;
|
||||
return baseUrl('/settings/users/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url that links to this user's profile.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProfileUrl()
|
||||
{
|
||||
return baseUrl('/user/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
157
app/helpers.php
157
app/helpers.php
@@ -1,64 +1,58 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('versioned_asset')) {
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
function versioned_asset($file)
|
||||
{
|
||||
static $manifest = null;
|
||||
use BookStack\Ownable;
|
||||
|
||||
if (is_null($manifest)) {
|
||||
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
function versioned_asset($file = '')
|
||||
{
|
||||
// Don't require css and JS assets for testing
|
||||
if (config('app.env') === 'testing') return '';
|
||||
|
||||
static $manifest = null;
|
||||
$manifestPath = 'build/manifest.json';
|
||||
|
||||
if (is_null($manifest) && file_exists($manifestPath)) {
|
||||
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
|
||||
} else if (!file_exists($manifestPath)) {
|
||||
if (config('app.env') !== 'production') {
|
||||
$path = public_path($manifestPath);
|
||||
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
|
||||
} else {
|
||||
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
|
||||
}
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return '/' . $manifest[$file];
|
||||
}
|
||||
|
||||
if (file_exists(public_path($file))) {
|
||||
return '/' . $file;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||
throw new \Exception($error);
|
||||
}
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return baseUrl($manifest[$file]);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has a permission.
|
||||
* If an ownable element is passed in the permissions are checked against
|
||||
* If an ownable element is passed in the jointPermissions are checked against
|
||||
* that particular item.
|
||||
* @param $permission
|
||||
* @param \BookStack\Ownable $ownable
|
||||
* @param Ownable $ownable
|
||||
* @return mixed
|
||||
*/
|
||||
function userCan($permission, \BookStack\Ownable $ownable = null)
|
||||
function userCan($permission, Ownable $ownable = null)
|
||||
{
|
||||
if (!auth()->check()) return false;
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissionBaseName = strtolower($permission) . '-';
|
||||
$hasPermission = false;
|
||||
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
|
||||
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
|
||||
|
||||
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
|
||||
|
||||
// Check restrictions on the entity
|
||||
$restrictionService = app('BookStack\Services\RestrictionService');
|
||||
$explodedPermission = explode('-', $permission);
|
||||
$action = end($explodedPermission);
|
||||
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
|
||||
$restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
|
||||
return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
|
||||
$permissionService = app(\BookStack\Services\PermissionService::class);
|
||||
return $permissionService->checkOwnableUserAccess($ownable, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,3 +66,82 @@ function setting($key, $default = false)
|
||||
$settingService = app('BookStack\Services\SettingService');
|
||||
return $settingService->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create url's relative to the applications root path.
|
||||
* @param string $path
|
||||
* @param bool $forceAppDomain
|
||||
* @return string
|
||||
*/
|
||||
function baseUrl($path, $forceAppDomain = false)
|
||||
{
|
||||
$isFullUrl = strpos($path, 'http') === 0;
|
||||
if ($isFullUrl && !$forceAppDomain) return $path;
|
||||
$path = trim($path, '/');
|
||||
|
||||
if ($isFullUrl && $forceAppDomain) {
|
||||
$explodedPath = explode('/', $path);
|
||||
$path = implode('/', array_splice($explodedPath, 3));
|
||||
}
|
||||
|
||||
// Return normal url path if not specified in config
|
||||
if (config('app.url') === '') {
|
||||
return url($path);
|
||||
}
|
||||
|
||||
return rtrim(config('app.url'), '/') . '/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the redirector.
|
||||
* Overrides the default laravel redirect helper.
|
||||
* Ensures it redirects even when the app is in a subdirectory.
|
||||
*
|
||||
* @param string|null $to
|
||||
* @param int $status
|
||||
* @param array $headers
|
||||
* @param bool $secure
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
function redirect($to = null, $status = 302, $headers = [], $secure = null)
|
||||
{
|
||||
if (is_null($to)) {
|
||||
return app('redirect');
|
||||
}
|
||||
|
||||
$to = baseUrl($to);
|
||||
|
||||
return app('redirect')->to($to, $status, $headers, $secure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a url with multiple parameters for sorting purposes.
|
||||
* Works out the logic to set the correct sorting direction
|
||||
* Discards empty parameters and allows overriding.
|
||||
* @param $path
|
||||
* @param array $data
|
||||
* @param array $overrideData
|
||||
* @return string
|
||||
*/
|
||||
function sortUrl($path, $data, $overrideData = [])
|
||||
{
|
||||
$queryStringSections = [];
|
||||
$queryData = array_merge($data, $overrideData);
|
||||
|
||||
// Change sorting direction is already sorted on current attribute
|
||||
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
||||
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
||||
} else {
|
||||
$queryData['order'] = 'asc';
|
||||
}
|
||||
|
||||
foreach ($queryData as $name => $value) {
|
||||
$trimmedVal = trim($value);
|
||||
if ($trimmedVal === '') continue;
|
||||
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
|
||||
}
|
||||
|
||||
if (count($queryStringSections) === 0) return $path;
|
||||
|
||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||
}
|
||||
@@ -14,6 +14,7 @@ define('LARAVEL_START', microtime(true));
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../app/helpers.php';
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|
||||
@@ -29,10 +29,7 @@
|
||||
],
|
||||
"psr-4": {
|
||||
"BookStack\\": "app/"
|
||||
},
|
||||
"files": [
|
||||
"app/helpers.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
|
||||
@@ -31,7 +31,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -130,7 +130,6 @@ return [
|
||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||
Illuminate\Hashing\HashServiceProvider::class,
|
||||
Illuminate\Mail\MailServiceProvider::class,
|
||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||
Illuminate\Queue\QueueServiceProvider::class,
|
||||
Illuminate\Redis\RedisServiceProvider::class,
|
||||
@@ -153,6 +152,8 @@ return [
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
BookStack\Providers\PaginationServiceProvider::class,
|
||||
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
|
||||
@@ -84,8 +84,8 @@ return [
|
||||
'driver' => 'mysql',
|
||||
'host' => 'localhost',
|
||||
'database' => 'bookstack-test',
|
||||
'username' => 'bookstack-test',
|
||||
'password' => 'bookstack-test',
|
||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_unicode_ci',
|
||||
'prefix' => '',
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
*/
|
||||
return [
|
||||
|
||||
'app-editor' => 'wysiwyg'
|
||||
'app-name' => 'BookStack',
|
||||
'app-editor' => 'wysiwyg',
|
||||
'app-color' => '#0288D1',
|
||||
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
];
|
||||
@@ -52,4 +52,11 @@ $factory->define(BookStack\Role::class, function ($faker) {
|
||||
'display_name' => $faker->sentence(3),
|
||||
'description' => $faker->sentence(10)
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Tag::class, function ($faker) {
|
||||
return [
|
||||
'name' => $faker->city,
|
||||
'value' => $faker->sentence(3)
|
||||
];
|
||||
});
|
||||
@@ -21,10 +21,13 @@ class CreateUsersTable extends Migration
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
\BookStack\User::forceCreate([
|
||||
// Create the initial admin user
|
||||
DB::table('users')->insert([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => bcrypt('password')
|
||||
'password' => bcrypt('password'),
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ class CreateBooksTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('books', function (Blueprint $table) {
|
||||
$pdo = \DB::connection()->getPdo();
|
||||
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
|
||||
|
||||
Schema::create('books', function (Blueprint $table) use ($requiresISAM) {
|
||||
if($requiresISAM) $table->engine = 'MyISAM';
|
||||
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->string('slug')->indexed();
|
||||
|
||||
@@ -12,7 +12,13 @@ class CreatePagesTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('pages', function (Blueprint $table) {
|
||||
$pdo = \DB::connection()->getPdo();
|
||||
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
|
||||
|
||||
Schema::create('pages', function (Blueprint $table) use ($requiresISAM) {
|
||||
if($requiresISAM) $table->engine = 'MyISAM';
|
||||
|
||||
$table->increments('id');
|
||||
$table->integer('book_id');
|
||||
$table->integer('chapter_id');
|
||||
|
||||
@@ -12,7 +12,12 @@ class CreateChaptersTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('chapters', function (Blueprint $table) {
|
||||
$pdo = \DB::connection()->getPdo();
|
||||
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
|
||||
|
||||
Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) {
|
||||
if($requiresISAM) $table->engine = 'MyISAM';
|
||||
$table->increments('id');
|
||||
$table->integer('book_id');
|
||||
$table->string('slug')->indexed();
|
||||
|
||||
@@ -68,35 +68,44 @@ class AddRolesAndPermissions extends Migration
|
||||
|
||||
|
||||
// Create default roles
|
||||
$admin = new \BookStack\Role();
|
||||
$admin->name = 'admin';
|
||||
$admin->display_name = 'Admin';
|
||||
$admin->description = 'Administrator of the whole application';
|
||||
$admin->save();
|
||||
$adminId = DB::table('roles')->insertGetId([
|
||||
'name' => 'admin',
|
||||
'display_name' => 'Admin',
|
||||
'description' => 'Administrator of the whole application',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
$editorId = DB::table('roles')->insertGetId([
|
||||
'name' => 'editor',
|
||||
'display_name' => 'Editor',
|
||||
'description' => 'User can edit Books, Chapters & Pages',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
$viewerId = DB::table('roles')->insertGetId([
|
||||
'name' => 'viewer',
|
||||
'display_name' => 'Viewer',
|
||||
'description' => 'User can view books & their content behind authentication',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
|
||||
$editor = new \BookStack\Role();
|
||||
$editor->name = 'editor';
|
||||
$editor->display_name = 'Editor';
|
||||
$editor->description = 'User can edit Books, Chapters & Pages';
|
||||
$editor->save();
|
||||
|
||||
$viewer = new \BookStack\Role();
|
||||
$viewer->name = 'viewer';
|
||||
$viewer->display_name = 'Viewer';
|
||||
$viewer->description = 'User can view books & their content behind authentication';
|
||||
$viewer->save();
|
||||
|
||||
// Create default CRUD permissions and allocate to admins and editors
|
||||
$entities = ['Book', 'Page', 'Chapter', 'Image'];
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity . 's';
|
||||
$newPermission->save();
|
||||
$admin->attachPermission($newPermission);
|
||||
$editor->attachPermission($newPermission);
|
||||
$newPermId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
['permission_id' => $newPermId, 'role_id' => $adminId],
|
||||
['permission_id' => $newPermId, 'role_id' => $editorId]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +114,27 @@ class AddRolesAndPermissions extends Migration
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity;
|
||||
$newPermission->save();
|
||||
$admin->attachPermission($newPermission);
|
||||
$newPermId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'permission_id' => $newPermId,
|
||||
'role_id' => $adminId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set all current users as admins
|
||||
// (At this point only the initially create user should be an admin)
|
||||
$users = \BookStack\User::all();
|
||||
$users = DB::table('users')->get();
|
||||
foreach ($users as $user) {
|
||||
$user->attachRole($admin);
|
||||
DB::table('role_user')->insert([
|
||||
'role_id' => $adminId,
|
||||
'user_id' => $user->id
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,29 +13,31 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
public function up()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
$editorRole = \BookStack\Role::getRole('editor');
|
||||
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
|
||||
$editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
|
||||
|
||||
// Delete old permissions
|
||||
$permissions = \BookStack\Permission::all();
|
||||
$permissions->each(function ($permission) {
|
||||
$permission->delete();
|
||||
});
|
||||
$permissions = DB::table('permissions')->delete();
|
||||
|
||||
// Create & attach new admin permissions
|
||||
$permissionsToCreate = [
|
||||
'settings-manage' => 'Manage Settings',
|
||||
'users-manage' => 'Manage Users',
|
||||
'user-roles-manage' => 'Manage Roles & Permissions',
|
||||
'restrictions-manage-all' => 'Manage All Entity Restrictions',
|
||||
'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content'
|
||||
'restrictions-manage-all' => 'Manage All Entity Permissions',
|
||||
'restrictions-manage-own' => 'Manage Entity Permissions On Own Content'
|
||||
];
|
||||
foreach ($permissionsToCreate as $name => $displayName) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = $name;
|
||||
$newPermission->display_name = $displayName;
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => $name,
|
||||
'display_name' => $displayName,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
|
||||
// Create & attach new entity permissions
|
||||
@@ -43,12 +45,22 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
|
||||
$newPermission->display_name = $op . ' ' . $entity . 's';
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
if ($editorRole !== null) $editorRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
if ($editorRole !== null) {
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $editorRole->id,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,24 +74,26 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
public function down()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
|
||||
|
||||
// Delete old permissions
|
||||
$permissions = \BookStack\Permission::all();
|
||||
$permissions->each(function ($permission) {
|
||||
$permission->delete();
|
||||
});
|
||||
$permissions = DB::table('permissions')->delete();
|
||||
|
||||
// Create default CRUD permissions and allocate to admins and editors
|
||||
$entities = ['Book', 'Page', 'Chapter', 'Image'];
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity . 's';
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +102,16 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity;
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddViewPermissionsToRoles extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$currentRoles = DB::table('roles')->get();
|
||||
|
||||
// Create new view permission
|
||||
$entities = ['Book', 'Page', 'Chapter'];
|
||||
$ops = ['View All', 'View Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$permId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
// Assign view permission to all current roles
|
||||
foreach ($currentRoles as $role) {
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Delete the new view permission
|
||||
$entities = ['Book', 'Page', 'Chapter'];
|
||||
$ops = ['View All', 'View Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$permissionName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
|
||||
$permission = DB::table('permissions')->where('name', '=', $permissionName)->first();
|
||||
DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete();
|
||||
DB::table('permissions')->where('name', '=', $permissionName)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateJointPermissionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('joint_permissions', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('role_id');
|
||||
$table->string('entity_type');
|
||||
$table->integer('entity_id');
|
||||
$table->string('action');
|
||||
$table->boolean('has_permission')->default(false);
|
||||
$table->boolean('has_permission_own')->default(false);
|
||||
$table->integer('created_by');
|
||||
// Create indexes
|
||||
$table->index(['entity_id', 'entity_type']);
|
||||
$table->index('has_permission');
|
||||
$table->index('has_permission_own');
|
||||
$table->index('role_id');
|
||||
$table->index('action');
|
||||
$table->index('created_by');
|
||||
});
|
||||
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->string('system_name');
|
||||
$table->boolean('hidden')->default(false);
|
||||
$table->index('hidden');
|
||||
$table->index('system_name');
|
||||
});
|
||||
|
||||
Schema::rename('permissions', 'role_permissions');
|
||||
Schema::rename('restrictions', 'entity_permissions');
|
||||
|
||||
// Create the new public role
|
||||
$publicRoleData = [
|
||||
'name' => 'public',
|
||||
'display_name' => 'Public',
|
||||
'description' => 'The role given to public visitors if allowed',
|
||||
'system_name' => 'public',
|
||||
'hidden' => true,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
];
|
||||
|
||||
// Ensure unique name
|
||||
while (DB::table('roles')->where('name', '=', $publicRoleData['display_name'])->count() > 0) {
|
||||
$publicRoleData['display_name'] = $publicRoleData['display_name'] . str_random(2);
|
||||
}
|
||||
$publicRoleId = DB::table('roles')->insertGetId($publicRoleData);
|
||||
|
||||
// Add new view permissions to public role
|
||||
$entities = ['Book', 'Page', 'Chapter'];
|
||||
$ops = ['View All', 'View Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
|
||||
$permission = DB::table('role_permissions')->where('name', '=', $name)->first();
|
||||
// Assign view permission to public
|
||||
DB::table('permission_role')->insert([
|
||||
'permission_id' => $permission->id,
|
||||
'role_id' => $publicRoleId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update admin role with system name
|
||||
DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
|
||||
|
||||
// Generate the new entity jointPermissions
|
||||
$restrictionService = app(\BookStack\Services\PermissionService::class);
|
||||
$restrictionService->buildJointPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop('joint_permissions');
|
||||
|
||||
Schema::rename('role_permissions', 'permissions');
|
||||
Schema::rename('entity_permissions', 'restrictions');
|
||||
|
||||
// Delete the public role
|
||||
DB::table('roles')->where('system_name', '=', 'public')->delete();
|
||||
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->dropColumn('system_name');
|
||||
$table->dropColumn('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
40
database/migrations/2016_05_06_185215_create_tags_table.php
Normal file
40
database/migrations/2016_05_06_185215_create_tags_table.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateTagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('tags', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('entity_id');
|
||||
$table->string('entity_type', 100);
|
||||
$table->string('name');
|
||||
$table->string('value');
|
||||
$table->integer('order');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('name');
|
||||
$table->index('value');
|
||||
$table->index('order');
|
||||
$table->index(['entity_id', 'entity_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop('tags');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSummaryToPageRevisions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('page_revisions', function ($table) {
|
||||
$table->string('summary')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('page_revisions', function ($table) {
|
||||
$table->dropColumn('summary');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,15 @@ class DummyContentSeeder extends Seeder
|
||||
->each(function($book) use ($user) {
|
||||
$chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($chapter) use ($user, $book){
|
||||
$pages = factory(\BookStack\Page::class, 10)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
|
||||
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
|
||||
$chapter->pages()->saveMany($pages);
|
||||
});
|
||||
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
|
||||
$book->chapters()->saveMany($chapters);
|
||||
$book->pages()->saveMany($pages);
|
||||
});
|
||||
|
||||
$restrictionService = app(\BookStack\Services\PermissionService::class);
|
||||
$restrictionService->buildJointPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
11
package.json
11
package.json
@@ -4,14 +4,15 @@
|
||||
"gulp": "^3.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"angular": "^1.5.0-rc.0",
|
||||
"angular-animate": "^1.5.0-rc.0",
|
||||
"angular-resource": "^1.5.0-rc.0",
|
||||
"angular-sanitize": "^1.5.0-rc.0",
|
||||
"angular": "^1.5.5",
|
||||
"angular-animate": "^1.5.5",
|
||||
"angular-resource": "^1.5.5",
|
||||
"angular-sanitize": "^1.5.5",
|
||||
"angular-ui-sortable": "^0.14.0",
|
||||
"babel-runtime": "^5.8.29",
|
||||
"bootstrap-sass": "^3.0.0",
|
||||
"dropzone": "^4.0.1",
|
||||
"laravel-elixir": "^3.4.0",
|
||||
"laravel-elixir": "^5.0.0",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.12.0",
|
||||
"zeroclipboard": "^2.2.0"
|
||||
|
||||
@@ -28,11 +28,12 @@
|
||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||
<env name="MAIL_DRIVER" value="log"/>
|
||||
<env name="AUTH_METHOD" value="standard"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||
<env name="LDAP_VERSION" value="3"/>
|
||||
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="APP_URL" value="http://bookstack.dev"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"css/styles.css": "css/styles.css?version=83f4ada",
|
||||
"css/print-styles.css": "css/print-styles.css?version=83f4ada",
|
||||
"js/common.js": "js/common.js?version=83f4ada"
|
||||
"css/styles.css": "css/styles.css?version=5be13a8",
|
||||
"css/print-styles.css": "css/print-styles.css?version=5be13a8",
|
||||
"js/common.js": "js/common.js?version=5be13a8"
|
||||
}
|
||||
2
public/css/export-styles.css
vendored
2
public/css/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/print-styles.css
vendored
2
public/css/print-styles.css
vendored
@@ -1 +1 @@
|
||||
.faded-small,.print-hidden,header{display:none}body{font-size:12px}.page-content{margin:0 auto}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
header{display:none}body{font-size:12px}.faded-small{display:none}.page-content{margin:0 auto}.print-hidden{display:none}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
2
public/css/styles.css
vendored
2
public/css/styles.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
public/libs/jquery/jquery-ui.min.js
vendored
Normal file
7
public/libs/jquery/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
tinymce.PluginManager.add("advlist",function(a){function b(a,b){var c=[];return tinymce.each(b.split(/[ ,]/),function(a){c.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),c}function c(b,c){a.undoManager.transact(function(){var d,e=a.dom,f=a.selection;d=e.getParent(f.getNode(),"ol,ul"),d&&d.nodeName==b&&c!==!1||a.execCommand("UL"==b?"InsertUnorderedList":"InsertOrderedList"),c=c===!1?g[b]:c,g[b]=c,d=e.getParent(f.getNode(),"ol,ul"),d&&(e.setStyle(d,"listStyleType",c?c:null),d.removeAttribute("data-mce-style")),a.focus()})}function d(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var e,f,g={};e=b("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),f=b("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square")),a.addButton("numlist",{type:"splitbutton",tooltip:"Numbered list",menu:e,onshow:d,onselect:function(a){c("OL",a.control.settings.data)},onclick:function(){c("OL",!1)}}),a.addButton("bullist",{type:"splitbutton",tooltip:"Bullet list",menu:f,onshow:d,onselect:function(a){c("UL",a.control.settings.data)},onclick:function(){c("UL",!1)}})});
|
||||
tinymce.PluginManager.add("advlist",function(a){function b(a,b){var c=[];return tinymce.each(b.split(/[ ,]/),function(a){c.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),c}function c(b,c){a.undoManager.transact(function(){var d,e=a.dom,f=a.selection;if(d=e.getParent(f.getNode(),"ol,ul"),!d||d.nodeName!=b||c===!1){var h={"list-style-type":c?c:""};a.execCommand("UL"==b?"InsertUnorderedList":"InsertOrderedList",!1,h)}c=c===!1?g[b]:c,g[b]=c,d=e.getParent(f.getNode(),"ol,ul"),d&&(e.setStyle(d,"listStyleType",c?c:null),d.removeAttribute("data-mce-style")),a.focus()})}function d(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var e,f,g={};e=b("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),f=b("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square")),a.addButton("numlist",{type:"splitbutton",tooltip:"Numbered list",menu:e,onshow:d,onselect:function(a){c("OL",a.control.settings.data)},onclick:function(){c("OL",!1)}}),a.addButton("bullist",{type:"splitbutton",tooltip:"Bullet list",menu:f,onshow:d,onselect:function(a){c("UL",a.control.settings.data)},onclick:function(){c("UL",!1)}})});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
tinymce.PluginManager.add("contextmenu",function(a){var b,c=a.settings.contextmenu_never_use_native;a.on("contextmenu",function(d){var e,f=a.getDoc();if(!d.ctrlKey||c){if(d.preventDefault(),tinymce.Env.mac&&tinymce.Env.webkit&&2==d.button&&f.caretRangeFromPoint&&a.selection.setRng(f.caretRangeFromPoint(d.x,d.y)),e=a.settings.contextmenu||"link image inserttable | cell row column deletetable",b)b.show();else{var g=[];tinymce.each(e.split(/[ ,]/),function(b){var c=a.menuItems[b];"|"==b&&(c={text:b}),c&&(c.shortcut="",g.push(c))});for(var h=0;h<g.length;h++)"|"==g[h].text&&(0!==h&&h!=g.length-1||g.splice(h,1));b=new tinymce.ui.Menu({items:g,context:"contextmenu",classes:"contextmenu"}).renderTo(),a.on("remove",function(){b.remove(),b=null})}var i={x:d.pageX,y:d.pageY};a.inline||(i=tinymce.DOM.getPos(a.getContentAreaContainer()),i.x+=d.clientX,i.y+=d.clientY),b.moveTo(i.x,i.y)}})});
|
||||
tinymce.PluginManager.add("contextmenu",function(a){var b,c=a.settings.contextmenu_never_use_native,d=function(a){return a.ctrlKey&&!c},e=function(){return tinymce.Env.mac&&tinymce.Env.webkit};a.on("mousedown",function(b){e()&&2===b.button&&!d(b)&&a.selection.isCollapsed()&&a.once("contextmenu",function(b){a.selection.placeCaretAt(b.clientX,b.clientY)})}),a.on("contextmenu",function(c){var e;if(!d(c)){if(c.preventDefault(),e=a.settings.contextmenu||"link image inserttable | cell row column deletetable",b)b.show();else{var f=[];tinymce.each(e.split(/[ ,]/),function(b){var c=a.menuItems[b];"|"==b&&(c={text:b}),c&&(c.shortcut="",f.push(c))});for(var g=0;g<f.length;g++)"|"==f[g].text&&(0!==g&&g!=f.length-1||f.splice(g,1));b=new tinymce.ui.Menu({items:f,context:"contextmenu",classes:"contextmenu"}).renderTo(),a.on("remove",function(){b.remove(),b=null})}var h={x:c.pageX,y:c.pageY};a.inline||(h=tinymce.DOM.getPos(a.getContentAreaContainer()),h.x+=c.clientX,h.y+=c.clientY),b.moveTo(h.x,h.y)}})});
|
||||
@@ -1 +1 @@
|
||||
tinymce.PluginManager.add("fullscreen",function(a){function b(){var a,b,c=window,d=document,e=d.body;return e.offsetWidth&&(a=e.offsetWidth,b=e.offsetHeight),c.innerWidth&&c.innerHeight&&(a=c.innerWidth,b=c.innerHeight),{w:a,h:b}}function c(){var a=tinymce.DOM.getViewPort();return{x:a.x,y:a.y}}function d(a){scrollTo(a.x,a.y)}function e(){function e(){m.setStyle(p,"height",b().h-(o.clientHeight-p.clientHeight))}var n,o,p,q,r=document.body,s=document.documentElement;l=!l,o=a.getContainer(),n=o.style,p=a.getContentAreaContainer().firstChild,q=p.style,l?(k=c(),f=q.width,g=q.height,q.width=q.height="100%",i=n.width,j=n.height,n.width=n.height="",m.addClass(r,"mce-fullscreen"),m.addClass(s,"mce-fullscreen"),m.addClass(o,"mce-fullscreen"),m.bind(window,"resize",e),e(),h=e):(q.width=f,q.height=g,i&&(n.width=i),j&&(n.height=j),m.removeClass(r,"mce-fullscreen"),m.removeClass(s,"mce-fullscreen"),m.removeClass(o,"mce-fullscreen"),m.unbind(window,"resize",h),d(k)),a.fire("FullscreenStateChanged",{state:l})}var f,g,h,i,j,k,l=!1,m=tinymce.DOM;return a.settings.inline?void 0:(a.on("init",function(){a.addShortcut("Meta+Alt+F","",e)}),a.on("remove",function(){h&&m.unbind(window,"resize",h)}),a.addCommand("mceFullScreen",e),a.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Meta+Alt+F",selectable:!0,onClick:function(){e(),a.focus()},onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})},context:"view"}),a.addButton("fullscreen",{tooltip:"Fullscreen",shortcut:"Meta+Alt+F",onClick:e,onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})}}),{isFullscreen:function(){return l}})});
|
||||
tinymce.PluginManager.add("fullscreen",function(a){function b(){var a,b,c=window,d=document,e=d.body;return e.offsetWidth&&(a=e.offsetWidth,b=e.offsetHeight),c.innerWidth&&c.innerHeight&&(a=c.innerWidth,b=c.innerHeight),{w:a,h:b}}function c(){var a=tinymce.DOM.getViewPort();return{x:a.x,y:a.y}}function d(a){scrollTo(a.x,a.y)}function e(){function e(){m.setStyle(p,"height",b().h-(o.clientHeight-p.clientHeight))}var n,o,p,q,r=document.body,s=document.documentElement;l=!l,o=a.getContainer(),n=o.style,p=a.getContentAreaContainer().firstChild,q=p.style,l?(k=c(),f=q.width,g=q.height,q.width=q.height="100%",i=n.width,j=n.height,n.width=n.height="",m.addClass(r,"mce-fullscreen"),m.addClass(s,"mce-fullscreen"),m.addClass(o,"mce-fullscreen"),m.bind(window,"resize",e),e(),h=e):(q.width=f,q.height=g,i&&(n.width=i),j&&(n.height=j),m.removeClass(r,"mce-fullscreen"),m.removeClass(s,"mce-fullscreen"),m.removeClass(o,"mce-fullscreen"),m.unbind(window,"resize",h),d(k)),a.fire("FullscreenStateChanged",{state:l})}var f,g,h,i,j,k,l=!1,m=tinymce.DOM;return a.settings.inline?void 0:(a.on("init",function(){a.addShortcut("Ctrl+Shift+F","",e)}),a.on("remove",function(){h&&m.unbind(window,"resize",h)}),a.addCommand("mceFullScreen",e),a.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,onClick:function(){e(),a.focus()},onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})},context:"view"}),a.addButton("fullscreen",{tooltip:"Fullscreen",shortcut:"Ctrl+Shift+F",onClick:e,onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})}}),{isFullscreen:function(){return l}})});
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
tinymce.PluginManager.add("importcss",function(a){function b(a){var b=tinymce.Env.cacheSuffix;return"string"==typeof a&&(a=a.replace("?"+b,"").replace("&"+b,"")),a}function c(b){var c=a.settings,d=c.skin!==!1?c.skin||"lightgray":!1;if(d){var e=c.skin_url;return e=e?a.documentBaseURI.toAbsolute(e):tinymce.baseURL+"/skins/"+d,b===e+"/content"+(a.inline?".inline":"")+".min.css"}return!1}function d(a){return"string"==typeof a?function(b){return-1!==b.indexOf(a)}:a instanceof RegExp?function(b){return a.test(b)}:a}function e(d,e){function f(a,d){var i,j=a.href;if(j=b(j),j&&e(j,d)&&!c(j)){h(a.imports,function(a){f(a,!0)});try{i=a.cssRules||a.rules}catch(k){}h(i,function(a){a.styleSheet?f(a.styleSheet,!0):a.selectorText&&h(a.selectorText.split(","),function(a){g.push(tinymce.trim(a))})})}}var g=[],i={};h(a.contentCSS,function(a){i[a]=!0}),e||(e=function(a,b){return b||i[a]});try{h(d.styleSheets,function(a){f(a)})}catch(j){}return g}function f(b){var c,d=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(b);if(d){var e=d[1],f=d[2].substr(1).split(".").join(" "),g=tinymce.makeMap("a,img");return d[1]?(c={title:b},a.schema.getTextBlockElements()[e]?c.block=e:a.schema.getBlockElements()[e]||g[e.toLowerCase()]?c.selector=e:c.inline=e):d[2]&&(c={inline:"span",title:b.substr(1),classes:f}),a.settings.importcss_merge_classes!==!1?c.classes=f:c.attributes={"class":f},c}}var g=this,h=tinymce.each;a.on("renderFormatsMenu",function(b){var c=a.settings,i={},j=c.importcss_selector_converter||f,k=d(c.importcss_selector_filter),l=b.control;a.settings.importcss_append||l.items().remove();var m=[];tinymce.each(c.importcss_groups,function(a){a=tinymce.extend({},a),a.filter=d(a.filter),m.push(a)}),h(e(b.doc||a.getDoc(),d(c.importcss_file_filter)),function(b){if(-1===b.indexOf(".mce-")&&!i[b]&&(!k||k(b))){var c,d=j.call(g,b);if(d){var e=d.name||tinymce.DOM.uniqueId();if(m)for(var f=0;f<m.length;f++)if(!m[f].filter||m[f].filter(b)){m[f].item||(m[f].item={text:m[f].title,menu:[]}),c=m[f].item.menu;break}a.formatter.register(e,d);var h=tinymce.extend({},l.settings.itemDefaults,{text:d.title,format:e});c?c.push(h):l.add(h)}i[b]=!0}}),h(m,function(a){l.add(a.item)}),b.control.renderNew()}),g.convertSelectorToFormat=f});
|
||||
tinymce.PluginManager.add("importcss",function(a){function b(a){var b=tinymce.Env.cacheSuffix;return"string"==typeof a&&(a=a.replace("?"+b,"").replace("&"+b,"")),a}function c(b){var c=a.settings,d=c.skin!==!1?c.skin||"lightgray":!1;if(d){var e=c.skin_url;return e=e?a.documentBaseURI.toAbsolute(e):tinymce.baseURL+"/skins/"+d,b===e+"/content"+(a.inline?".inline":"")+".min.css"}return!1}function d(a){return"string"==typeof a?function(b){return-1!==b.indexOf(a)}:a instanceof RegExp?function(b){return a.test(b)}:a}function e(d,e){function f(a,d){var h,i=a.href;if(i=b(i),i&&e(i,d)&&!c(i)){n(a.imports,function(a){f(a,!0)});try{h=a.cssRules||a.rules}catch(j){}n(h,function(a){a.styleSheet?f(a.styleSheet,!0):a.selectorText&&n(a.selectorText.split(","),function(a){g.push(tinymce.trim(a))})})}}var g=[],h={};n(a.contentCSS,function(a){h[a]=!0}),e||(e=function(a,b){return b||h[a]});try{n(d.styleSheets,function(a){f(a)})}catch(i){}return g}function f(b){var c,d=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(b);if(d){var e=d[1],f=d[2].substr(1).split(".").join(" "),g=tinymce.makeMap("a,img");return d[1]?(c={title:b},a.schema.getTextBlockElements()[e]?c.block=e:a.schema.getBlockElements()[e]||g[e.toLowerCase()]?c.selector=e:c.inline=e):d[2]&&(c={inline:"span",title:b.substr(1),classes:f}),a.settings.importcss_merge_classes!==!1?c.classes=f:c.attributes={"class":f},c}}function g(a,b){return tinymce.util.Tools.grep(a,function(a){return!a.filter||a.filter(b)})}function h(a){return tinymce.util.Tools.map(a,function(a){return tinymce.util.Tools.extend({},a,{original:a,selectors:{},filter:d(a.filter),item:{text:a.title,menu:[]}})})}function i(a,b){return null===b||a.settings.importcss_exclusive!==!1}function j(b,c,d){return!(i(a,c)?b in d:b in c.selectors)}function k(b,c,d){i(a,c)?d[b]=!0:c.selectors[b]=!0}function l(b,c,d){var e,g=a.settings;return e=d&&d.selector_converter?d.selector_converter:g.importcss_selector_converter?g.importcss_selector_converter:f,e.call(b,c,d)}var m=this,n=tinymce.each;a.on("renderFormatsMenu",function(b){var c=a.settings,f={},i=d(c.importcss_selector_filter),o=b.control,p=h(c.importcss_groups),q=function(b,c){if(j(b,c,f)){k(b,c,f);var d=l(m,b,c);if(d){var e=d.name||tinymce.DOM.uniqueId();return a.formatter.register(e,d),tinymce.extend({},o.settings.itemDefaults,{text:d.title,format:e})}}return null};a.settings.importcss_append||o.items().remove(),n(e(b.doc||a.getDoc(),d(c.importcss_file_filter)),function(a){if(-1===a.indexOf(".mce-")&&(!i||i(a))){var b=g(p,a);if(b.length>0)tinymce.util.Tools.each(b,function(b){var c=q(a,b);c&&b.item.menu.push(c)});else{var c=q(a,null);c&&o.add(c)}}}),n(p,function(a){a.item.menu.length>0&&o.add(a.item)}),b.control.renderNew()}),m.convertSelectorToFormat=f});
|
||||
@@ -1 +1 @@
|
||||
!function(a){a.on("AddEditor",function(a){a.editor.settings.inline_styles=!1}),a.PluginManager.add("legacyoutput",function(b,c,d){b.on("init",function(){var c="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img",d=a.explode(b.settings.font_size_style_values),e=b.schema;b.formatter.register({alignleft:{selector:c,attributes:{align:"left"}},aligncenter:{selector:c,attributes:{align:"center"}},alignright:{selector:c,attributes:{align:"right"}},alignjustify:{selector:c,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all"},{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all"},{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all"},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all"},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",attributes:{face:"%value"}},fontsize:{inline:"font",attributes:{size:function(b){return a.inArray(d,b.value)+1}}},forecolor:{inline:"font",attributes:{color:"%value"}},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"}}}),a.each("b,i,u,strike".split(","),function(a){e.addValidElements(a+"[*]")}),e.getElementRule("font")||e.addValidElements("font[face|size|color|style]"),a.each(c.split(","),function(a){var b=e.getElementRule(a);b&&(b.attributes.align||(b.attributes.align={},b.attributesOrder.push("align")))})}),b.addButton("fontsizeselect",function(){var a=[],c="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7",d=b.settings.fontsize_formats||c;return b.$.each(d.split(" "),function(b,c){var d=c,e=c,f=c.split("=");f.length>1&&(d=f[0],e=f[1]),a.push({text:d,value:e})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:a,fixedWidth:!0,onPostRender:function(){var a=this;b.on("NodeChange",function(){var c;c=b.dom.getParent(b.selection.getNode(),"font"),c?a.value(c.size):a.value("")})},onclick:function(a){a.control.settings.value&&b.execCommand("FontSize",!1,a.control.settings.value)}}}),b.addButton("fontselect",function(){function a(a){a=a.replace(/;$/,"").split(";");for(var b=a.length;b--;)a[b]=a[b].split("=");return a}var c="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",e=[],f=a(b.settings.font_formats||c);return d.each(f,function(a,b){e.push({text:{raw:b[0]},value:b[1],textStyle:-1==b[1].indexOf("dings")?"font-family:"+b[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:function(){var a=this;b.on("NodeChange",function(){var c;c=b.dom.getParent(b.selection.getNode(),"font"),c?a.value(c.face):a.value("")})},onselect:function(a){a.control.settings.value&&b.execCommand("FontName",!1,a.control.settings.value)}}})})}(tinymce);
|
||||
!function(a){a.PluginManager.add("legacyoutput",function(b,c,d){b.settings.inline_styles=!1,b.on("init",function(){var c="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img",d=a.explode(b.settings.font_size_style_values),e=b.schema;b.formatter.register({alignleft:{selector:c,attributes:{align:"left"}},aligncenter:{selector:c,attributes:{align:"center"}},alignright:{selector:c,attributes:{align:"right"}},alignjustify:{selector:c,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all"},{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all"},{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all"},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all"},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",attributes:{face:"%value"}},fontsize:{inline:"font",attributes:{size:function(b){return a.inArray(d,b.value)+1}}},forecolor:{inline:"font",attributes:{color:"%value"}},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"}}}),a.each("b,i,u,strike".split(","),function(a){e.addValidElements(a+"[*]")}),e.getElementRule("font")||e.addValidElements("font[face|size|color|style]"),a.each(c.split(","),function(a){var b=e.getElementRule(a);b&&(b.attributes.align||(b.attributes.align={},b.attributesOrder.push("align")))})}),b.addButton("fontsizeselect",function(){var a=[],c="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7",d=b.settings.fontsize_formats||c;return b.$.each(d.split(" "),function(b,c){var d=c,e=c,f=c.split("=");f.length>1&&(d=f[0],e=f[1]),a.push({text:d,value:e})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:a,fixedWidth:!0,onPostRender:function(){var a=this;b.on("NodeChange",function(){var c;c=b.dom.getParent(b.selection.getNode(),"font"),c?a.value(c.size):a.value("")})},onclick:function(a){a.control.settings.value&&b.execCommand("FontSize",!1,a.control.settings.value)}}}),b.addButton("fontselect",function(){function a(a){a=a.replace(/;$/,"").split(";");for(var b=a.length;b--;)a[b]=a[b].split("=");return a}var c="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",e=[],f=a(b.settings.font_formats||c);return d.each(f,function(a,b){e.push({text:{raw:b[0]},value:b[1],textStyle:-1==b[1].indexOf("dings")?"font-family:"+b[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:function(){var a=this;b.on("NodeChange",function(){var c;c=b.dom.getParent(b.selection.getNode(),"font"),c?a.value(c.face):a.value("")})},onselect:function(a){a.control.settings.value&&b.execCommand("FontName",!1,a.control.settings.value)}}})})}(tinymce);
|
||||
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
@@ -1 +1 @@
|
||||
tinymce.PluginManager.add("preview",function(a){var b=a.settings,c=!tinymce.Env.ie;a.addCommand("mcePreview",function(){a.windowManager.open({title:"Preview",width:parseInt(a.getParam("plugin_preview_width","650"),10),height:parseInt(a.getParam("plugin_preview_height","500"),10),html:'<iframe src="javascript:\'\'" frameborder="0"'+(c?' sandbox="allow-scripts"':"")+"></iframe>",buttons:{text:"Close",onclick:function(){this.parent().parent().close()}},onPostRender:function(){var d,e="";e+='<base href="'+a.documentBaseURI.getURI()+'">',tinymce.each(a.contentCSS,function(b){e+='<link type="text/css" rel="stylesheet" href="'+a.documentBaseURI.toAbsolute(b)+'">'});var f=b.body_id||"tinymce";-1!=f.indexOf("=")&&(f=a.getParam("body_id","","hash"),f=f[a.id]||f);var g=b.body_class||"";-1!=g.indexOf("=")&&(g=a.getParam("body_class","","hash"),g=g[a.id]||"");var h=a.settings.directionality?' dir="'+a.settings.directionality+'"':"";if(d="<!DOCTYPE html><html><head>"+e+'</head><body id="'+f+'" class="mce-content-body '+g+'"'+h+">"+a.getContent()+"</body></html>",c)this.getEl("body").firstChild.src="data:text/html;charset=utf-8,"+encodeURIComponent(d);else{var i=this.getEl("body").firstChild.contentWindow.document;i.open(),i.write(d),i.close()}}})}),a.addButton("preview",{title:"Preview",cmd:"mcePreview"}),a.addMenuItem("preview",{text:"Preview",cmd:"mcePreview",context:"view"})});
|
||||
tinymce.PluginManager.add("preview",function(a){var b=a.settings,c=!tinymce.Env.ie;a.addCommand("mcePreview",function(){a.windowManager.open({title:"Preview",width:parseInt(a.getParam("plugin_preview_width","650"),10),height:parseInt(a.getParam("plugin_preview_height","500"),10),html:'<iframe src="javascript:\'\'" frameborder="0"'+(c?' sandbox="allow-scripts"':"")+"></iframe>",buttons:{text:"Close",onclick:function(){this.parent().parent().close()}},onPostRender:function(){var d,e="";e+='<base href="'+a.documentBaseURI.getURI()+'">',tinymce.each(a.contentCSS,function(b){e+='<link type="text/css" rel="stylesheet" href="'+a.documentBaseURI.toAbsolute(b)+'">'});var f=b.body_id||"tinymce";-1!=f.indexOf("=")&&(f=a.getParam("body_id","","hash"),f=f[a.id]||f);var g=b.body_class||"";-1!=g.indexOf("=")&&(g=a.getParam("body_class","","hash"),g=g[a.id]||"");var h='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A") {e.preventDefault();}}}, false);</script> ',i=a.settings.directionality?' dir="'+a.settings.directionality+'"':"";if(d="<!DOCTYPE html><html><head>"+e+'</head><body id="'+f+'" class="mce-content-body '+g+'"'+i+">"+a.getContent()+h+"</body></html>",c)this.getEl("body").firstChild.src="data:text/html;charset=utf-8,"+encodeURIComponent(d);else{var j=this.getEl("body").firstChild.contentWindow.document;j.open(),j.write(d),j.close()}}})}),a.addButton("preview",{title:"Preview",cmd:"mcePreview"}),a.addMenuItem("preview",{text:"Preview",cmd:"mcePreview",context:"view"})});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user