mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-14 03:09:39 +03:00
Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
4e8722661f | ||
|
|
6f9d2939e7 | ||
|
|
3234510d31 | ||
|
|
a40af08018 | ||
|
|
223a6b546c | ||
|
|
0d5da2d9d4 | ||
|
|
37337b8b1d | ||
|
|
ffcc058927 | ||
|
|
3a1cda5802 | ||
|
|
5c1015d6fc | ||
|
|
3385ec3f78 | ||
|
|
1b46c19849 | ||
|
|
75a4fc905b | ||
|
|
05666efda9 | ||
|
|
59367b3417 | ||
|
|
9a31b83b2a | ||
|
|
a81a56706e | ||
|
|
9dc1b35e82 | ||
|
|
2dc1180a41 | ||
|
|
ada7c83e96 | ||
|
|
ea287ebf86 | ||
|
|
043cdeafb3 | ||
|
|
8b36ca95a3 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
8933179017 | ||
|
|
792e786880 | ||
|
|
753f6394f7 | ||
|
|
0a8030306e | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
d6bad01130 | ||
|
|
a33deed26b | ||
|
|
2e7345f4f0 | ||
|
|
6e03078de3 | ||
|
|
1a7de4c2d6 | ||
|
|
66ba773367 | ||
|
|
afc3583be8 | ||
|
|
cbff2c6035 | ||
|
|
8e614ecb6e | ||
|
|
d099885fd1 | ||
|
|
2bb8c3d914 | ||
|
|
4caa61fe96 | ||
|
|
c5960f9b6a | ||
|
|
412eed19c3 | ||
|
|
e9b596d3bc | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
8b109bac13 | ||
|
|
097d9c9f3c | ||
|
|
e7d8a041a8 | ||
|
|
dc2978824e | ||
|
|
e1994ef2cf | ||
|
|
efb49019d4 | ||
|
|
ef874712bb | ||
|
|
26965fa08f | ||
|
|
1fe933e4ea | ||
|
|
491f73e0cd | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
4656c12f6d | ||
|
|
a06321675a | ||
|
|
dbe11c1360 | ||
|
|
75ecf1c44d | ||
|
|
5283919d24 | ||
|
|
ced8c8e497 | ||
|
|
bf7852ce85 | ||
|
|
30214fde74 | ||
|
|
e9c213f803 | ||
|
|
9f11e045a5 | ||
|
|
93ebdf724b | ||
|
|
59ce228c2e | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
1d6137f7e2 | ||
|
|
66c56e9d02 | ||
|
|
e744d4c82c | ||
|
|
0774ecc89c | ||
|
|
5e7a4c7fb5 | ||
|
|
76eaf64f94 | ||
|
|
80865b30a5 | ||
|
|
8e6248f57f | ||
|
|
268db6b1d0 | ||
|
|
479dd80a8c | ||
|
|
069431db72 | ||
|
|
bc2b310638 | ||
|
|
33bf20cfc8 | ||
|
|
e3bdc391cd | ||
|
|
5681f4dd69 | ||
|
|
38d822e04c | ||
|
|
8e274a5a84 | ||
|
|
985d2f1c2c | ||
|
|
7f5872372d | ||
|
|
201f788806 | ||
|
|
a14b5c33fd | ||
|
|
473261be35 | ||
|
|
a54be85185 |
17
.env.example
17
.env.example
@@ -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
|
||||
@@ -12,8 +16,17 @@ DB_PASSWORD=database_user_password
|
||||
# Cache and session
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
# If using Memcached, comment the above and uncomment these
|
||||
#CACHE_DRIVER=memcached
|
||||
#SESSION_DRIVER=memcached
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# Memcached settings
|
||||
# If using a UNIX socket path for the host, set the port to 0
|
||||
# This follows the following format: HOST:PORT:WEIGHT
|
||||
# For multiple servers separate with a comma
|
||||
MEMCACHED_SERVERS=127.0.0.1:11211:100
|
||||
|
||||
# Storage
|
||||
STORAGE_TYPE=local
|
||||
# Amazon S3 Config
|
||||
@@ -33,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
|
||||
@@ -53,4 +64,4 @@ MAIL_HOST=localhost
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_ENCRYPTION=null
|
||||
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
|
||||
@@ -15,15 +13,11 @@ class Activity extends Model
|
||||
|
||||
/**
|
||||
* Get the entity for this activity.
|
||||
* @return bool
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
if ($this->entity_id) {
|
||||
return $this->morphTo('entity')->first();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if ($this->entity_type === '') $this->entity_type = null;
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +26,7 @@ class Activity extends Model
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
152
app/Entity.php
152
app/Entity.php
@@ -1,14 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Entity extends Model
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
use Ownable;
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
@@ -48,16 +43,64 @@ abstract class Entity extends Model
|
||||
*/
|
||||
public function activity()
|
||||
{
|
||||
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
|
||||
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get View objects for this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
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 permissions()
|
||||
{
|
||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this entity has a specific restriction set against it.
|
||||
* @param $role_id
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRestriction($role_id, $action)
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,27 +111,43 @@ abstract class Entity extends Model
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return static::getClassName() === strtolower($type);
|
||||
return static::getType() === strtolower($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class name.
|
||||
* @return string
|
||||
* Get entity type.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getClassName()
|
||||
public static function getType()
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
return strtolower(static::getClassName());
|
||||
}
|
||||
|
||||
/**
|
||||
*Gets a limited-length version of the entities name.
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a limited-length version of the entities name.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($length = 25)
|
||||
{
|
||||
if(strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length-3) . '...';
|
||||
if (strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,36 +157,55 @@ abstract class Entity extends Model
|
||||
* @param string[] array $wheres
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$termString = '';
|
||||
foreach ($terms as $term) {
|
||||
$termString .= htmlentities($term) . '* ';
|
||||
}
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
|
||||
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
$exactTerms = [];
|
||||
if (count($terms) === 0) {
|
||||
$search = $this;
|
||||
$orderBy = 'updated_at';
|
||||
} else {
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/".*?"/', $term)) {
|
||||
$term = str_replace('"', '', $term);
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
$term = '"' . $term . '"';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
}
|
||||
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]);
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$orderBy = 'title_relevance';
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
18
app/EntityPermission.php
Normal file
18
app/EntityPermission.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['role_id', 'action'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get all this restriction's attached entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function restrictable()
|
||||
{
|
||||
return $this->morphTo('restrictable');
|
||||
}
|
||||
}
|
||||
4
app/Exceptions/AuthException.php
Normal file
4
app/Exceptions/AuthException.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class AuthException extends PrettyException {}
|
||||
@@ -47,18 +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();
|
||||
return response()->view('errors/500', ['message' => $message], 500);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
14
app/Exceptions/NotFoundException.php
Normal file
14
app/Exceptions/NotFoundException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class NotFoundException extends PrettyException {
|
||||
|
||||
/**
|
||||
* NotFoundException constructor.
|
||||
* @param string $message
|
||||
*/
|
||||
public function __construct($message = 'Item not found')
|
||||
{
|
||||
parent::__construct($message, 404);
|
||||
}
|
||||
}
|
||||
6
app/Exceptions/PermissionsException.php
Normal file
6
app/Exceptions/PermissionsException.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class PermissionsException extends Exception {}
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
class PrettyException extends \Exception {}
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
<?php namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
@@ -34,16 +33,15 @@ class AuthController extends Controller
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new authentication controller instance.
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param EmailConfirmationService $emailConfirmationService
|
||||
* @param UserRepo $userRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
@@ -51,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();
|
||||
}
|
||||
@@ -63,15 +63,15 @@ class AuthController extends Controller
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'password' => 'required|min:6',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function checkRegistrationAllowed()
|
||||
{
|
||||
if (!\Setting::get('registration-enabled')) {
|
||||
if (!setting('registration-enabled')) {
|
||||
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
|
||||
}
|
||||
}
|
||||
@@ -112,9 +112,10 @@ class AuthController extends Controller
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
@@ -132,12 +133,21 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
return redirect()->intended($this->redirectPath());
|
||||
$path = session()->pull('url.intended', '/');
|
||||
$path = baseUrl($path, true);
|
||||
return redirect($path);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,8 +163,8 @@ class AuthController extends Controller
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
@@ -162,7 +172,7 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* The registrations flow for all users.
|
||||
* @param array $userData
|
||||
* @param array $userData
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
@@ -170,8 +180,8 @@ class AuthController extends Controller
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
{
|
||||
if (\Setting::get('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict')));
|
||||
if (setting('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
|
||||
@@ -183,15 +193,12 @@ class AuthController extends Controller
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) {
|
||||
$newUser->email_confirmed = false;
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
$newUser->save();
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
$newUser->email_confirmed = true;
|
||||
|
||||
auth()->login($newUser);
|
||||
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
||||
return redirect($this->redirectPath());
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
@@ -19,24 +15,26 @@ class BookController extends Controller
|
||||
protected $bookRepo;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
@@ -50,12 +48,11 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for creating a new book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('book-create');
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->setPageTitle('Create New Book');
|
||||
return view('books/create');
|
||||
}
|
||||
@@ -68,29 +65,25 @@ class BookController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('book-create');
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
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());
|
||||
@@ -99,37 +92,32 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($slug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->setPageTitle('Edit Book ' . $book->getShortName());
|
||||
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified book in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request, $slug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'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());
|
||||
}
|
||||
@@ -141,8 +129,8 @@ class BookController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->setPageTitle('Delete Book ' . $book->getShortName());
|
||||
return view('books/delete', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
@@ -154,9 +142,9 @@ class BookController extends Controller
|
||||
*/
|
||||
public function sort($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->bookRepo->getChildren($book, true);
|
||||
$books = $this->bookRepo->getAll(false);
|
||||
$this->setPageTitle('Sort Book ' . $book->getShortName());
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
@@ -177,36 +165,45 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Saves an array of sort mapping to pages and chapters.
|
||||
*
|
||||
* @param string $bookSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function saveSort($bookSlug, Request $request)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
// Return if no map sent
|
||||
if (!$request->has('sort-tree')) {
|
||||
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;
|
||||
}
|
||||
@@ -218,22 +215,56 @@ class BookController extends Controller
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
|
||||
// Update permissions on changed models
|
||||
$this->bookRepo->buildJointPermissions($updatedModels);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified book from storage.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function destroy($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('books/restrictions', [
|
||||
'book' => $book,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this book.
|
||||
* @param $bookSlug
|
||||
* @param $bookSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
|
||||
session()->flash('success', 'Book Restrictions Updated');
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use Views;
|
||||
@@ -17,20 +13,22 @@ class ChapterController extends Controller
|
||||
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* ChapterController constructor.
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for creating a new chapter.
|
||||
* @param $bookSlug
|
||||
@@ -38,8 +36,8 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function create($bookSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-create');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
$this->setPageTitle('Create New Chapter');
|
||||
return view('chapters/create', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
@@ -52,18 +50,16 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function store($bookSlug, Request $request)
|
||||
{
|
||||
$this->checkPermission('chapter-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$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);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
|
||||
$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());
|
||||
}
|
||||
@@ -78,10 +74,18 @@ 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());
|
||||
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
|
||||
$pages = $this->chapterRepo->getChildren($chapter);
|
||||
return view('chapters/show', [
|
||||
'book' => $book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,9 +96,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function edit($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
|
||||
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
@@ -108,9 +112,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$chapter->fill($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
@@ -127,9 +131,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
|
||||
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
@@ -142,11 +146,103 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
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
|
||||
* @param $chapterSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('chapters/restrictions', [
|
||||
'chapter' => $chapter,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $chapterSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
|
||||
session()->flash('success', 'Chapter Restrictions Updated');
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Ownable;
|
||||
use HttpRequestException;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Exception\HttpResponseException;
|
||||
@@ -61,21 +62,19 @@ abstract class Controller extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* On a permission error redirect to home and display
|
||||
* On a permission error redirect to home and display.
|
||||
* the error as a notification.
|
||||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
throw new HttpResponseException(
|
||||
redirect('/')
|
||||
);
|
||||
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a permission.
|
||||
*
|
||||
* @param $permissionName
|
||||
* @param string $permissionName
|
||||
* @return bool|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function checkPermission($permissionName)
|
||||
@@ -83,10 +82,21 @@ abstract class Controller extends BaseController
|
||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the current user's permissions against an ownable item.
|
||||
* @param $permission
|
||||
* @param Ownable $ownable
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkOwnablePermission($permission, Ownable $ownable)
|
||||
{
|
||||
if (userCan($permission, $ownable)) return true;
|
||||
return $this->showPermissionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a permission or bypass if the callback is true.
|
||||
* @param $permissionName
|
||||
@@ -100,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,20 +24,22 @@ class HomeController extends Controller
|
||||
|
||||
/**
|
||||
* Display the homepage.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$activity = Activity::latest(10);
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12, 0) : $this->entityRepo->getRecentlyCreatedBooks(10);
|
||||
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor);
|
||||
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
|
||||
return view('home', [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyCreatedPages' => $recentlyCreatedPages,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Repos\ImageRepo;
|
||||
use Illuminate\Filesystem\Filesystem as File;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Intervention\Image\Facades\Image as ImageTool;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use BookStack\Image;
|
||||
use BookStack\Repos\PageRepo;
|
||||
|
||||
@@ -20,8 +15,8 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* ImageController constructor.
|
||||
* @param Image $image
|
||||
* @param File $file
|
||||
* @param Image $image
|
||||
* @param File $file
|
||||
* @param ImageRepo $imageRepo
|
||||
*/
|
||||
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
|
||||
@@ -32,9 +27,9 @@ class ImageController extends Controller
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all images for a specific type, Paginated
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
@@ -44,6 +39,24 @@ class ImageController extends Controller
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through images within a particular type.
|
||||
* @param $type
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchByType($type, $page = 0, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'term' => 'required|string'
|
||||
]);
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all images for a user.
|
||||
* @param int $page
|
||||
@@ -55,24 +68,45 @@ class ImageController extends Controller
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery images with a specific filter such as book or page
|
||||
* @param $filter
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
*/
|
||||
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
$validFilters = collect(['page', 'book']);
|
||||
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||
|
||||
$pageId = $request->get('page_id');
|
||||
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles image uploads for use on pages.
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function uploadByType($type, Request $request)
|
||||
{
|
||||
$this->checkPermission('image-create');
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => 'image|mimes:jpeg,gif,png'
|
||||
'file' => 'is_image'
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
|
||||
try {
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type);
|
||||
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
@@ -90,7 +124,7 @@ class ImageController extends Controller
|
||||
*/
|
||||
public function getThumbnail($id, $width, $height, $crop)
|
||||
{
|
||||
$this->checkPermission('image-create');
|
||||
$this->checkPermission('image-create-all');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
|
||||
return response()->json(['url' => $thumbnailUrl]);
|
||||
@@ -98,33 +132,32 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Update image details
|
||||
* @param $imageId
|
||||
* @param integer $imageId
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update($imageId, Request $request)
|
||||
{
|
||||
$this->checkPermission('image-update');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|min:2|string'
|
||||
]);
|
||||
$image = $this->imageRepo->getById($imageId);
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes an image and all thumbnail/image files
|
||||
* @param PageRepo $pageRepo
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy(PageRepo $pageRepo, Request $request, $id)
|
||||
{
|
||||
$this->checkPermission('image-delete');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('image-delete', $image);
|
||||
|
||||
// Check if this image is used on any pages
|
||||
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
@@ -21,26 +20,28 @@ class PageController extends Controller
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $exportService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param ExportService $exportService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->exportService = $exportService;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param bool $chapterSlug
|
||||
* @return Response
|
||||
@@ -48,33 +49,60 @@ class PageController extends Controller
|
||||
*/
|
||||
public function create($bookSlug, $chapterSlug = false)
|
||||
{
|
||||
$this->checkPermission('page-create');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
$this->setPageTitle('Create New Page');
|
||||
return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
|
||||
|
||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created page in storage.
|
||||
*
|
||||
* Show form to continue editing a draft page.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function editDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$draft = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $book);
|
||||
$this->setPageTitle('Edit Page Draft');
|
||||
|
||||
return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new page by changing a draft into a page.
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param string $bookSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request, $bookSlug)
|
||||
public function store(Request $request, $bookSlug, $pageId)
|
||||
{
|
||||
$this->checkPermission('page-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$input = $request->all();
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($book);
|
||||
|
||||
$page = $this->pageRepo->saveNew($input, $book, $chapterId);
|
||||
$draftPage = $this->pageRepo->getById($pageId, true);
|
||||
|
||||
$chapterId = intval($draftPage->chapter_id);
|
||||
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
if ($parent->isA('chapter')) {
|
||||
$input['priority'] = $this->chapterRepo->getNewPriority($parent);
|
||||
} else {
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($parent);
|
||||
}
|
||||
|
||||
$page = $this->pageRepo->publishDraft($draftPage, $input);
|
||||
|
||||
Activity::add($page, 'page_create', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
@@ -84,7 +112,6 @@ class PageController extends Controller
|
||||
* Display the specified page.
|
||||
* If the page is not found via the slug the
|
||||
* revisions are searched for a match.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return Response
|
||||
@@ -95,37 +122,68 @@ class PageController extends Controller
|
||||
|
||||
try {
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
} catch (NotFoundHttpException $e) {
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page from an ajax request.
|
||||
* @param $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getPageAjax($pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
return response()->json($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle('Editing Page ' . $page->getShortName());
|
||||
$page->isDraft = false;
|
||||
|
||||
// Check for active editing
|
||||
$warnings = [];
|
||||
if ($this->pageRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
$page->name = $draft->name;
|
||||
$page->html = $draft->html;
|
||||
$page->markdown = $draft->markdown;
|
||||
$page->isDraft = true;
|
||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||
|
||||
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified page in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
@@ -133,17 +191,42 @@ class PageController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->pageRepo->updatePage($page, $book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a draft update as a revision.
|
||||
* @param Request $request
|
||||
* @param $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function saveDraft(Request $request, $pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
if ($page->draft) {
|
||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||
} else {
|
||||
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
}
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Draft saved at ',
|
||||
'timestamp' => $utcUpdateTimestamp
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect from a special link url which
|
||||
* uses the page id rather than the name.
|
||||
@@ -164,16 +247,32 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->setPageTitle('Delete Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the deletion page for the specified page.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @return \Illuminate\View\View
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showDeleteDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle('Delete Draft Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified page from storage.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return Response
|
||||
@@ -181,10 +280,28 @@ class PageController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||
session()->flash('success', 'Page deleted');
|
||||
$this->pageRepo->destroy($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified draft page from storage.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function destroyDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
session()->flash('success', 'Draft deleted');
|
||||
$this->pageRepo->destroy($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -229,9 +346,9 @@ class PageController extends Controller
|
||||
*/
|
||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
@@ -250,8 +367,8 @@ class PageController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -267,8 +384,8 @@ class PageController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -284,8 +401,8 @@ class PageController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -295,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
|
||||
@@ -308,11 +425,107 @@ 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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('pages/restrictions', [
|
||||
'page' => $page,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view to choose a new parent to move a page into.
|
||||
* @param $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
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Permissions Updated');
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
132
app/Http/Controllers/PermissionController.php
Normal file
132
app/Http/Controllers/PermissionController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
|
||||
protected $permissionsRepo;
|
||||
|
||||
/**
|
||||
* PermissionController constructor.
|
||||
* @param PermissionsRepo $permissionsRepo
|
||||
*/
|
||||
public function __construct(PermissionsRepo $permissionsRepo)
|
||||
{
|
||||
$this->permissionsRepo = $permissionsRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of the roles in the system.
|
||||
*/
|
||||
public function listRoles()
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$roles = $this->permissionsRepo->getAllRoles();
|
||||
return view('settings/roles/index', ['roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form to create a new role
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function createRole()
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
return view('settings/roles/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new role in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function storeRole(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->saveNewRole($request->all());
|
||||
session()->flash('success', 'Role successfully created');
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a user role.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function updateRole($id, Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->updateRole($id, $request->all());
|
||||
session()->flash('success', 'Role successfully updated');
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view to delete a role.
|
||||
* Offers the chance to migrate users.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showDeleteRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
|
||||
$roles->prepend($blankRole);
|
||||
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the system,
|
||||
* Migrate from a previous role if set.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function deleteRole($id, Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
|
||||
try {
|
||||
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
|
||||
} catch (PermissionsException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Role successfully deleted');
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Setting;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the settings.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->checkPermission('settings-update');
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->setPageTitle('Settings');
|
||||
return view('settings/index');
|
||||
}
|
||||
|
||||
// Get application version
|
||||
$version = false;
|
||||
if (function_exists('exec')) {
|
||||
$version = exec('git describe --always --tags ');
|
||||
}
|
||||
|
||||
return view('settings/index', ['version' => $version]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified settings in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermission('settings-update');
|
||||
$this->checkPermission('settings-manage');
|
||||
|
||||
// Cycles through posted settings and update them
|
||||
foreach($request->all() as $name => $value) {
|
||||
if(strpos($name, 'setting-') !== 0) continue;
|
||||
foreach ($request->all() as $name => $value) {
|
||||
if (strpos($name, 'setting-') !== 0) continue;
|
||||
$key = str_replace('setting-', '', trim($name));
|
||||
Setting::put($key, $value);
|
||||
}
|
||||
|
||||
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,13 +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)
|
||||
{
|
||||
$users = $this->user->all();
|
||||
$this->checkPermission('users-manage');
|
||||
$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]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +54,10 @@ class UserController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
$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]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,11 +67,10 @@ class UserController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
$this->checkPermission('users-manage');
|
||||
$validationRules = [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'role' => 'required|exists:roles,id'
|
||||
'email' => 'required|email|unique:users,email'
|
||||
];
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
@@ -84,7 +92,11 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$user->attachRoleId($request->get('role'));
|
||||
|
||||
if ($request->has('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
@@ -104,7 +116,7 @@ class UserController extends Controller
|
||||
*/
|
||||
public function edit($id, SocialAuthService $socialAuthService)
|
||||
{
|
||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
@@ -113,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]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +138,7 @@ class UserController extends Controller
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
@@ -133,8 +146,7 @@ class UserController extends Controller
|
||||
'name' => 'min:2',
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password',
|
||||
'role' => 'exists:roles,id'
|
||||
'password-confirm' => 'same:password|required_with:password'
|
||||
], [
|
||||
'password-confirm.required_with' => 'Password confirmation required'
|
||||
]);
|
||||
@@ -143,8 +155,9 @@ class UserController extends Controller
|
||||
$user->fill($request->all());
|
||||
|
||||
// Role updates
|
||||
if ($this->currentUser->can('user-update') && $request->has('role')) {
|
||||
$user->attachRoleId($request->get('role'));
|
||||
if (userCan('users-manage') && $request->has('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Password updates
|
||||
@@ -154,12 +167,15 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
// External auth id updates
|
||||
if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
|
||||
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return redirect('/settings/users');
|
||||
session()->flash('success', 'User successfully updated');
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||
return redirect($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +185,7 @@ class UserController extends Controller
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
@@ -186,16 +202,19 @@ class UserController extends Controller
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
@@ -11,14 +11,12 @@ class Authenticate
|
||||
{
|
||||
/**
|
||||
* The Guard implementation.
|
||||
*
|
||||
* @var Guard
|
||||
*/
|
||||
protected $auth;
|
||||
|
||||
/**
|
||||
* Create a new filter instance.
|
||||
*
|
||||
* @param Guard $auth
|
||||
*/
|
||||
public function __construct(Guard $auth)
|
||||
@@ -28,22 +26,21 @@ class Authenticate
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if(auth()->check() && auth()->user()->email_confirmed == false) {
|
||||
return redirect()->guest('/register/confirm/awaiting');
|
||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||
return redirect()->guest(baseUrl('/register/confirm/awaiting'));
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !Setting::get('app-public')) {
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
return redirect()->guest('/login');
|
||||
return redirect()->guest(baseUrl('/login'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,21 +19,30 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::delete('/{id}', 'BookController@destroy');
|
||||
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
|
||||
Route::get('/{slug}', 'BookController@show');
|
||||
Route::get('/{bookSlug}/permissions', 'BookController@showRestrict');
|
||||
Route::put('/{bookSlug}/permissions', 'BookController@restrict');
|
||||
Route::get('/{slug}/delete', 'BookController@showDelete');
|
||||
Route::get('/{bookSlug}/sort', 'BookController@sort');
|
||||
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
|
||||
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
Route::post('/{bookSlug}/page', 'PageController@store');
|
||||
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
|
||||
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');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@restrict');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
|
||||
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
|
||||
Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
|
||||
|
||||
// Revisions
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
|
||||
@@ -46,7 +55,11 @@ 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');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
|
||||
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
|
||||
|
||||
@@ -66,9 +79,26 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
||||
Route::get('/{type}/all', 'ImageController@getAllByType');
|
||||
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
||||
Route::get('/{type}/search/{page}', 'ImageController@searchByType');
|
||||
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
|
||||
Route::delete('/{imageId}', 'ImageController@destroy');
|
||||
});
|
||||
|
||||
// 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');
|
||||
|
||||
@@ -87,6 +117,7 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::group(['prefix' => 'settings'], function() {
|
||||
Route::get('/', 'SettingController@index');
|
||||
Route::post('/', 'SettingController@update');
|
||||
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
@@ -95,6 +126,15 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
Route::delete('/users/{id}', 'UserController@destroy');
|
||||
|
||||
// Roles
|
||||
Route::get('/roles', 'PermissionController@listRoles');
|
||||
Route::get('/roles/new', 'PermissionController@createRole');
|
||||
Route::post('/roles/new', 'PermissionController@storeRole');
|
||||
Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
|
||||
Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
|
||||
Route::get('/roles/{id}', 'PermissionController@editRole');
|
||||
Route::put('/roles/{id}', 'PermissionController@updateRole');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Images;
|
||||
|
||||
class Image extends Model
|
||||
class Image extends Ownable
|
||||
{
|
||||
use Ownable;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
|
||||
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,7 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
trait Ownable
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
/**
|
||||
* Relation for the user that created this entity.
|
||||
@@ -9,7 +9,7 @@ trait Ownable
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,6 +18,16 @@ trait Ownable
|
||||
*/
|
||||
public function updatedBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'updated_by');
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class name.
|
||||
* @return string
|
||||
*/
|
||||
public static function getClassName()
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
}
|
||||
|
||||
}
|
||||
54
app/Page.php
54
app/Page.php
@@ -1,15 +1,16 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'priority'];
|
||||
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
||||
|
||||
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,32 +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')->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;
|
||||
return '/books/' . $bookSlug . '/page/' . $this->slug;
|
||||
$midText = $this->draft ? '/draft/' : '/page/';
|
||||
$idComponent = $this->draft ? $this->id : $this->slug;
|
||||
|
||||
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,23 +1,32 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text'];
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this revision originates from.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Page');
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this revision.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permissions');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -28,24 +35,31 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
return new ActivityService($this->app->make('BookStack\Activity'));
|
||||
return new ActivityService(
|
||||
$this->app->make(Activity::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
return new ViewService($this->app->make('BookStack\View'));
|
||||
return new ViewService(
|
||||
$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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class LdapUserProvider implements UserProvider
|
||||
$model->name = $userDetails['name'];
|
||||
$model->external_auth_id = $userDetails['uid'];
|
||||
$model->email = $userDetails['email'];
|
||||
$model->email_confirmed = true;
|
||||
$model->email_confirmed = false;
|
||||
return $model;
|
||||
}
|
||||
|
||||
|
||||
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,28 +1,37 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Activity;
|
||||
use Alpha\B;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
use Views;
|
||||
|
||||
class BookRepo
|
||||
class BookRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $book;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* BookRepo constructor.
|
||||
* @param Book $book
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
*/
|
||||
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
{
|
||||
$this->book = $book;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting books.
|
||||
* Takes into account any restrictions.
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookQuery()
|
||||
{
|
||||
return $this->permissionService->enforceBookRestrictions($this->book, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +41,7 @@ class BookRepo
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->book->findOrFail($id);
|
||||
return $this->bookQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +51,7 @@ class BookRepo
|
||||
*/
|
||||
public function getAll($count = 10)
|
||||
{
|
||||
$bookQuery = $this->book->orderBy('name', 'asc');
|
||||
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
|
||||
if (!$count) return $bookQuery->get();
|
||||
return $bookQuery->take($count)->get();
|
||||
}
|
||||
@@ -54,7 +63,8 @@ class BookRepo
|
||||
*/
|
||||
public function getAllPaginated($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('name', 'asc')->paginate($count);
|
||||
return $this->bookQuery()
|
||||
->orderBy('name', 'asc')->paginate($count);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +75,7 @@ class BookRepo
|
||||
*/
|
||||
public function getLatest($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('created_at', 'desc')->take($count)->get();
|
||||
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,11 +104,12 @@ class BookRepo
|
||||
* Get a book by slug
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug)
|
||||
{
|
||||
$book = $this->book->where('slug', '=', $slug)->first();
|
||||
if ($book === null) abort(404);
|
||||
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
|
||||
if ($book === null) throw new NotFoundException('Book not found');
|
||||
return $book;
|
||||
}
|
||||
|
||||
@@ -109,36 +120,48 @@ class BookRepo
|
||||
*/
|
||||
public function exists($id)
|
||||
{
|
||||
return $this->book->where('id', '=', $id)->exists();
|
||||
return $this->bookQuery()->where('id', '=', $id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new book instance from request input.
|
||||
* @param array $input
|
||||
* @return Book
|
||||
*/
|
||||
public function createFromInput($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given book from user input.
|
||||
* @param Book $book
|
||||
* @param $input
|
||||
* @return Book
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
public function updateFromInput(Book $book, $input)
|
||||
{
|
||||
return $this->book->fill($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the amount of books that have a specific slug.
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
* Destroy the given book.
|
||||
* @param Book $book
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function countBySlug($slug)
|
||||
public function destroy(Book $book)
|
||||
{
|
||||
return $this->book->where('slug', '=', $slug)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a book identified by the given slug.
|
||||
* @param $bookSlug
|
||||
*/
|
||||
public function destroyBySlug($bookSlug)
|
||||
{
|
||||
$book = $this->getBySlug($bookSlug);
|
||||
foreach ($book->pages as $page) {
|
||||
$this->pageRepo->destroy($page);
|
||||
}
|
||||
@@ -146,6 +169,8 @@ class BookRepo
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
@@ -183,12 +208,10 @@ class BookRepo
|
||||
*/
|
||||
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,25 +219,54 @@ class BookRepo
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getChildren(Book $book)
|
||||
public function getChildren(Book $book, $filterDrafts = false)
|
||||
{
|
||||
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
|
||||
$chapters = $book->chapters()->with('pages')->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
|
||||
$pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
|
||||
if ($filterDrafts) {
|
||||
$pageQuery = $pageQuery->where('draft', '=', false);
|
||||
}
|
||||
|
||||
$pages = $pageQuery->get();
|
||||
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
|
||||
$this->permissionService->enforcePageRestrictions($query, 'view');
|
||||
if ($filterDrafts) $query->where('draft', '=', false);
|
||||
}]);
|
||||
$chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapters = $chapterQuery->get();
|
||||
$children = $pages->values();
|
||||
foreach ($chapters as $chapter) {
|
||||
$children->push($chapter);
|
||||
}
|
||||
$bookSlug = $book->slug;
|
||||
|
||||
$children->each(function ($child) use ($bookSlug) {
|
||||
$child->setAttribute('bookSlug', $bookSlug);
|
||||
if ($child->isA('chapter')) {
|
||||
$child->pages->each(function ($page) use ($bookSlug) {
|
||||
$page->setAttribute('bookSlug', $bookSlug);
|
||||
});
|
||||
$child->pages = $child->pages->sortBy(function ($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->draft) $score -= 100;
|
||||
return $score;
|
||||
});
|
||||
}
|
||||
});
|
||||
return $children->sortBy('priority');
|
||||
|
||||
// Sort items with drafts first then by priority.
|
||||
return $children->sortBy(function ($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->isA('page') && $child->draft) $score -= 100;
|
||||
return $score;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,9 +278,10 @@ class BookRepo
|
||||
*/
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$books = $this->book->fullTextSearchQuery(['name', 'description'], $terms)
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$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,21 +2,32 @@
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
||||
class ChapterRepo
|
||||
class ChapterRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $chapter;
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* ChapterRepo constructor.
|
||||
* @param $chapter
|
||||
* @param $pageRepo
|
||||
*/
|
||||
public function __construct(Chapter $chapter)
|
||||
public function __construct(PageRepo $pageRepo)
|
||||
{
|
||||
$this->chapter = $chapter;
|
||||
$this->pageRepo = $pageRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting chapters, Takes permissions into account.
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterQuery()
|
||||
{
|
||||
return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +37,7 @@ class ChapterRepo
|
||||
*/
|
||||
public function idExists($id)
|
||||
{
|
||||
return $this->chapter->where('id', '=', $id)->count() > 0;
|
||||
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +47,7 @@ class ChapterRepo
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->chapter->findOrFail($id);
|
||||
return $this->chapterQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +56,7 @@ class ChapterRepo
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->chapter->all();
|
||||
return $this->chapterQuery()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,22 +64,45 @@ class ChapterRepo
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$chapter = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($chapter === null) abort(404);
|
||||
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($chapter === null) throw new NotFoundException('Chapter not found');
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter
|
||||
* @param Chapter $chapter
|
||||
*/
|
||||
public function getChildren(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
|
||||
// Sort items with drafts first then by priority.
|
||||
return $pages->sortBy(function ($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->draft) $score -= 100;
|
||||
return $score;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,6 +119,8 @@ class ChapterRepo
|
||||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
@@ -115,15 +151,28 @@ class ChapterRepo
|
||||
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);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new priority value for a new page to be added
|
||||
* to the given chapter.
|
||||
* @param Chapter $chapter
|
||||
* @return int
|
||||
*/
|
||||
public function getNewPriority(Chapter $chapter)
|
||||
{
|
||||
$lastPage = $chapter->pages->last();
|
||||
return $lastPage !== null ? $lastPage->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chapters by the given search term.
|
||||
* @param $term
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
@@ -131,9 +180,10 @@ class ChapterRepo
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$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
|
||||
@@ -145,19 +195,32 @@ class ChapterRepo
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,69 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Book $book
|
||||
*/
|
||||
public $book;
|
||||
|
||||
/**
|
||||
* @var Chapter
|
||||
*/
|
||||
public $chapter;
|
||||
|
||||
/**
|
||||
* @var Page
|
||||
*/
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
* @param $book
|
||||
* @param $chapter
|
||||
* @param $page
|
||||
* @var PermissionService
|
||||
*/
|
||||
public function __construct(Book $book, Chapter $chapter, Page $page)
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->book = app(Book::class);
|
||||
$this->chapter = app(Chapter::class);
|
||||
$this->page = app(Page::class);
|
||||
$this->permissionService = app(PermissionService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest books added to the system.
|
||||
* @param $count
|
||||
* @param $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool $additionalQuery
|
||||
* @return
|
||||
*/
|
||||
public function getRecentlyCreatedBooks($count = 20, $page = 0)
|
||||
public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
return $this->book->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
$query = $this->permissionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,17 +74,42 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
|
||||
{
|
||||
return $this->book->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
return $this->permissionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
* @param $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool $additionalQuery
|
||||
* @return
|
||||
*/
|
||||
public function getRecentlyCreatedPages($count = 20, $page = 0)
|
||||
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
return $this->page->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
$query = $this->permissionService->enforcePageRestrictions($this->page)
|
||||
->orderBy('created_at', 'desc')->where('draft', '=', false);
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->with('book')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest chapters added to the system.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool $additionalQuery
|
||||
* @return
|
||||
*/
|
||||
public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceChapterRestrictions($this->chapter)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,8 +120,166 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->page->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
return $this->permissionService->enforcePageRestrictions($this->page)
|
||||
->where('draft', '=', false)
|
||||
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft pages owned by the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
*/
|
||||
public function getUserDraftPages($count = 20, $page = 0)
|
||||
{
|
||||
$user = auth()->user();
|
||||
return $this->page->where('draft', '=', true)
|
||||
->where('created_by', '=', $user->id)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity restrictions from a request
|
||||
* @param $request
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function updateEntityPermissionsFromRequest($request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
|
||||
$entity->permissions()->delete();
|
||||
if ($request->has('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->permissions()->create([
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a string of search terms by turning
|
||||
* it into an array of terms.
|
||||
* Keeps quoted terms together.
|
||||
* @param $termString
|
||||
* @return array
|
||||
*/
|
||||
public function prepareSearchTerms($termString)
|
||||
{
|
||||
$termString = $this->cleanSearchTermString($termString);
|
||||
preg_match_all('/(".*?")/', $termString, $matches);
|
||||
$terms = [];
|
||||
if (count($matches[1]) > 0) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$terms[] = $match;
|
||||
}
|
||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
@@ -11,16 +13,22 @@ class ImageRepo
|
||||
|
||||
protected $image;
|
||||
protected $imageService;
|
||||
protected $restrictionService;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
* @param Image $image
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
* @param PermissionService $permissionService
|
||||
* @param Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService)
|
||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
$this->restrictionService = $permissionService;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,23 +42,17 @@ class ImageRepo
|
||||
return $this->image->findOrFail($id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a load images paginated, filtered by image type.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool|int $userFilter
|
||||
* Execute a paginated query, returning in a standard format.
|
||||
* Also runs the query through the restriction system.
|
||||
* @param $query
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @return array
|
||||
*/
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
||||
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type));
|
||||
|
||||
if ($userFilter !== false) {
|
||||
$images = $images->where('created_by', '=', $userFilter);
|
||||
}
|
||||
|
||||
$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;
|
||||
|
||||
@@ -65,15 +67,74 @@ class ImageRepo
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a load images paginated, filtered by image type.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool|int $userFilter
|
||||
* @return array
|
||||
*/
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type));
|
||||
|
||||
if ($userFilter !== false) {
|
||||
$images = $images->where('created_by', '=', $userFilter);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for images by query, of a particular type.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param string $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery images with a particular filter criteria such as
|
||||
* being within the current book or page.
|
||||
* @param int $pagination
|
||||
* @param int $pageSize
|
||||
* @param $filter
|
||||
* @param $pageId
|
||||
* @return array
|
||||
*/
|
||||
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
|
||||
{
|
||||
$images = $this->image->where('type', '=', 'gallery');
|
||||
|
||||
$page = $this->page->findOrFail($pageId);
|
||||
|
||||
if ($filter === 'page') {
|
||||
$images = $images->where('uploaded_to', '=', $page->id);
|
||||
} elseif ($filter === 'book') {
|
||||
$validPageIds = $page->book->pages->pluck('id')->toArray();
|
||||
$images = $images->whereIn('uploaded_to', $validPageIds);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $pagination, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new image into storage and return the new image.
|
||||
* @param UploadedFile $uploadFile
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
*/
|
||||
public function saveNew(UploadedFile $uploadFile, $type)
|
||||
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
|
||||
{
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type);
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
|
||||
$this->loadThumbs($image);
|
||||
return $image;
|
||||
}
|
||||
@@ -123,9 +184,9 @@ class ImageRepo
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*
|
||||
* @param Image $image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @return string
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
|
||||
@@ -1,60 +1,57 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class PageRepo
|
||||
class PageRepo extends EntityRepo
|
||||
{
|
||||
protected $page;
|
||||
|
||||
protected $pageRevision;
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
* @param Page $page
|
||||
* @param PageRevision $pageRevision
|
||||
* @param TagRepo $tagRepo
|
||||
*/
|
||||
public function __construct(Page $page, PageRevision $pageRevision)
|
||||
public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
$this->tagRepo = $tagRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page id exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
* Base query for getting pages, Takes restrictions into account.
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function idExists($id)
|
||||
private function pageQuery($allowDrafts = false)
|
||||
{
|
||||
return $this->page->where('page_id', '=', $id)->count() > 0;
|
||||
$query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
|
||||
if (!$allowDrafts) {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page via a specific ID.
|
||||
* @param $id
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
public function getById($id, $allowDrafts = false)
|
||||
{
|
||||
return $this->page->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->page->all();
|
||||
return $this->pageQuery($allowDrafts)->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,11 +59,12 @@ class PageRepo
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($page === null) throw new NotFoundHttpException('Page not found');
|
||||
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($page === null) throw new NotFoundException('Page not found');
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -81,6 +79,10 @@ class PageRepo
|
||||
public function findPageUsingOldSlug($pageSlug, $bookSlug)
|
||||
{
|
||||
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function ($query) {
|
||||
$this->permissionService->enforcePageRestrictions($query);
|
||||
})
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
|
||||
->with('page')->first();
|
||||
return $revision !== null ? $revision->page : null;
|
||||
@@ -112,8 +114,8 @@ class PageRepo
|
||||
* Save a new page into the system.
|
||||
* Input validation must be done beforehand.
|
||||
* @param array $input
|
||||
* @param Book $book
|
||||
* @param int $chapterId
|
||||
* @param Book $book
|
||||
* @param int $chapterId
|
||||
* @return Page
|
||||
*/
|
||||
public function saveNew(array $input, Book $book, $chapterId = null)
|
||||
@@ -132,6 +134,55 @@ class PageRepo
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Publish a draft page to make it a normal page.
|
||||
* Sets the slug and updates the content.
|
||||
* @param Page $draftPage
|
||||
* @param array $input
|
||||
* @return Page
|
||||
*/
|
||||
public function publishDraft(Page $draftPage, array $input)
|
||||
{
|
||||
$draftPage->fill($input);
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
|
||||
}
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
* @param Chapter|bool $chapter
|
||||
* @return static
|
||||
*/
|
||||
public function getDraftPage(Book $book, $chapter = false)
|
||||
{
|
||||
$page = $this->page->newInstance();
|
||||
$page->name = 'New Page';
|
||||
$page->created_by = auth()->user()->id;
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->draft = true;
|
||||
|
||||
if ($chapter) $page->chapter_id = $chapter->id;
|
||||
|
||||
$book->pages()->save($page);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly
|
||||
* within the system.
|
||||
@@ -140,9 +191,9 @@ class PageRepo
|
||||
*/
|
||||
protected function formatHtml($htmlText)
|
||||
{
|
||||
if($htmlText == '') return $htmlText;
|
||||
if ($htmlText == '') return $htmlText;
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new \DOMDocument();
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$container = $doc->documentElement;
|
||||
@@ -201,9 +252,10 @@ class PageRepo
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$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), '/')));
|
||||
@@ -240,7 +292,7 @@ class PageRepo
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
@@ -251,29 +303,44 @@ class PageRepo
|
||||
|
||||
/**
|
||||
* Updates a page with any fillable data and saves it into the database.
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param string $input
|
||||
* @return Page
|
||||
*/
|
||||
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);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = auth()->user()->id;
|
||||
if (setting('app-editor') !== 'markdown') $page->markdown = '';
|
||||
$page->updated_by = $userId;
|
||||
$page->save();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -299,16 +366,20 @@ class PageRepo
|
||||
/**
|
||||
* 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 = '';
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
$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) {
|
||||
@@ -318,6 +389,155 @@ class PageRepo
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a page update draft.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return PageRevision
|
||||
*/
|
||||
public function saveUpdateDraft(Page $page, $data = [])
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
|
||||
|
||||
if ($drafts->count() > 0) {
|
||||
$draft = $drafts->first();
|
||||
} else {
|
||||
$draft = $this->pageRevision->newInstance();
|
||||
$draft->page_id = $page->id;
|
||||
$draft->slug = $page->slug;
|
||||
$draft->book_slug = $page->book->slug;
|
||||
$draft->created_by = $userId;
|
||||
$draft->type = 'update_draft';
|
||||
}
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a draft page.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return Page
|
||||
*/
|
||||
public function updateDraftPage(Page $page, $data = [])
|
||||
{
|
||||
$page->fill($data);
|
||||
|
||||
if (isset($data['html'])) {
|
||||
$page->text = strip_tags($data['html']);
|
||||
}
|
||||
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for getting user update drafts.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
private function userUpdateDraftsQuery(Page $page, $userId)
|
||||
{
|
||||
return $this->pageRevision->where('created_by', '=', $userId)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user has a draft version of a particular page or not.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUserGotPageDraft(Page $page, $userId)
|
||||
{
|
||||
return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest updated draft revision for a particular page and user.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserPageDraft(Page $page, $userId)
|
||||
{
|
||||
return $this->userUpdateDraftsQuery($page, $userId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification message that informs the user that they are editing a draft page.
|
||||
* @param PageRevision $draft
|
||||
* @return string
|
||||
*/
|
||||
public function getUserPageDraftMessage(PageRevision $draft)
|
||||
{
|
||||
$message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
|
||||
if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
|
||||
$message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is being actively editing.
|
||||
* Checks for edits since last page updated.
|
||||
* Passing in a minuted range will check for edits
|
||||
* within the last x minutes.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return bool
|
||||
*/
|
||||
public function isPageEditingActive(Page $page, $minRange = null)
|
||||
{
|
||||
$draftSearch = $this->activePageEditingQuery($page, $minRange);
|
||||
return $draftSearch->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notification message concerning the editing activity on
|
||||
* a particular page.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return string
|
||||
*/
|
||||
public function getPageEditingActiveMessage(Page $page, $minRange = null)
|
||||
{
|
||||
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
|
||||
$userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
|
||||
$timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
|
||||
$message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
|
||||
return sprintf($message, $userMessage, $timeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* A query to check for active update drafts on a particular page.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return mixed
|
||||
*/
|
||||
private function activePageEditingQuery(Page $page, $minRange = null)
|
||||
{
|
||||
$query = $this->pageRevision->where('type', '=', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->where('updated_at', '>', $page->updated_at)
|
||||
->where('created_by', '!=', auth()->user()->id)
|
||||
->with('createdBy');
|
||||
|
||||
if ($minRange !== null) {
|
||||
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single revision via it's id.
|
||||
* @param $id
|
||||
@@ -345,7 +565,7 @@ class PageRepo
|
||||
/**
|
||||
* Changes the related book for the specified page.
|
||||
* Changes the book id of any relations to the page that store the book id.
|
||||
* @param int $bookId
|
||||
* @param int $bookId
|
||||
* @param Page $page
|
||||
* @return Page
|
||||
*/
|
||||
@@ -361,16 +581,33 @@ class PageRepo
|
||||
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);
|
||||
}
|
||||
@@ -381,11 +618,14 @@ class PageRepo
|
||||
* 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->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
@@ -395,7 +635,7 @@ class PageRepo
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($count = 20)
|
||||
{
|
||||
return $this->page->orderBy('created_at', 'desc')->paginate($count);
|
||||
return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,7 +644,7 @@ class PageRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($count = 20)
|
||||
{
|
||||
return $this->page->orderBy('updated_at', 'desc')->paginate($count);
|
||||
return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
155
app/Repos/PermissionsRepo.php
Normal file
155
app/Repos/PermissionsRepo.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
|
||||
protected $systemRoles = ['admin', 'public'];
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the user roles from the system.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->where('hidden', '=', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles except for the provided one.
|
||||
* @param Role $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role)
|
||||
{
|
||||
return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role via its ID.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRoleById($id)
|
||||
{
|
||||
return $this->role->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new role into the system.
|
||||
* @param array $roleData
|
||||
* @return Role
|
||||
*/
|
||||
public function saveNewRole($roleData)
|
||||
{
|
||||
$role = $this->role->newInstance($roleData);
|
||||
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
|
||||
// Prevent duplicate names
|
||||
while ($this->role->where('name', '=', $role->name)->count() > 0) {
|
||||
$role->name .= strtolower(str_random(2));
|
||||
}
|
||||
$role->save();
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing role.
|
||||
* 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);
|
||||
|
||||
if ($role->name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign an list of permission names to an role.
|
||||
* @param Role $role
|
||||
* @param array $permissionNameArray
|
||||
*/
|
||||
public function assignRolePermissions(Role $role, $permissionNameArray = [])
|
||||
{
|
||||
$permissions = [];
|
||||
$permissionNameArray = array_values($permissionNameArray);
|
||||
if ($permissionNameArray && count($permissionNameArray) > 0) {
|
||||
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
|
||||
}
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the system.
|
||||
* Check it's not an admin role or set as default before deleting.
|
||||
* If an migration Role ID is specified the users assign to the current role
|
||||
* will be added to the role of the specified id.
|
||||
* @param $roleId
|
||||
* @param $migrateRoleId
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function deleteRole($roleId, $migrateRoleId)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
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.');
|
||||
}
|
||||
|
||||
if ($migrateRoleId) {
|
||||
$newRole = $this->role->find($migrateRoleId);
|
||||
if ($newRole) {
|
||||
$users = $role->users->pluck('id')->toArray();
|
||||
$newRole->users()->sync($users);
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,6 +42,36 @@ class UserRepo
|
||||
return $this->user->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsers()
|
||||
{
|
||||
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
|
||||
@@ -68,8 +98,8 @@ class UserRepo
|
||||
*/
|
||||
public function attachDefaultRole($user)
|
||||
{
|
||||
$roleId = Setting::get('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->getDefault()->id;
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->first()->id;
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
|
||||
@@ -80,15 +110,10 @@ class UserRepo
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
{
|
||||
if ($user->role->name != 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminRole = $this->role->where('name', '=', 'admin')->first();
|
||||
if (count($adminRole->users) > 1) {
|
||||
return false;
|
||||
}
|
||||
if (!$user->roles->pluck('name')->contains('admin')) return false;
|
||||
|
||||
$adminRole = $this->role->getRole('admin');
|
||||
if ($adminRole->users->count() > 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -102,7 +127,8 @@ class UserRepo
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password'])
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => false
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -137,12 +163,15 @@ class UserRepo
|
||||
public function getRecentlyCreated(User $user, $count = 20)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
|
||||
->take($count)->get(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
|
||||
->take($count)->get(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
|
||||
->take($count)->get()
|
||||
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
@@ -154,10 +183,29 @@ class UserRepo
|
||||
public function getAssetCounts(User $user)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
|
||||
}
|
||||
|
||||
}
|
||||
73
app/Role.php
73
app/Role.php
@@ -1,49 +1,66 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
/**
|
||||
* Sets the default role name for newly registered users.
|
||||
* @var string
|
||||
*/
|
||||
protected static $default = 'viewer';
|
||||
|
||||
protected $fillable = ['display_name', 'description'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
*/
|
||||
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 $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permissionName)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the default role.
|
||||
* @return Role
|
||||
* Detach a single permission from this role.
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public static function getDefault()
|
||||
public function detachPermission(RolePermission $permission)
|
||||
{
|
||||
return static::getRole(static::$default);
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
23
app/RolePermission.php
Normal file
23
app/RolePermission.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
* @param $name
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
{
|
||||
return static::where('name', '=', $name)->first();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Activity;
|
||||
use BookStack\Entity;
|
||||
use Session;
|
||||
@@ -9,14 +8,17 @@ class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param $activity
|
||||
* @param Activity $activity
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity)
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
@@ -24,8 +26,8 @@ class ActivityService
|
||||
* Add activity data to database.
|
||||
* @param Entity $entity
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param bool $extra
|
||||
* @param int $bookId
|
||||
* @param bool $extra
|
||||
*/
|
||||
public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
|
||||
{
|
||||
@@ -43,7 +45,7 @@ class ActivityService
|
||||
/**
|
||||
* Adds a activity history with a message & without binding to a entity.
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param int $bookId
|
||||
* @param bool|false $extra
|
||||
*/
|
||||
public function addMessage($activityKey, $bookId = 0, $extra = false)
|
||||
@@ -86,8 +88,10 @@ class ActivityService
|
||||
*/
|
||||
public function latest($count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->activity->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
|
||||
@@ -95,14 +99,22 @@ class ActivityService
|
||||
* Gets the latest activity for an entity, Filtering out similar
|
||||
* items to prevent a message activity list.
|
||||
* @param Entity $entity
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
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);
|
||||
}
|
||||
@@ -117,9 +129,10 @@ class ActivityService
|
||||
*/
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $this->activity->where('user_id', '=', $user->id)
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
return $this->filterSimilar($activity);
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ class EmailConfirmationService
|
||||
'token' => $token,
|
||||
]);
|
||||
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
|
||||
$appName = \Setting::get('app-name', 'BookStack');
|
||||
$appName = setting('app-name', 'BookStack');
|
||||
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -41,14 +41,16 @@ class ImageService
|
||||
/**
|
||||
* Saves a new image from an upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return mixed
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
|
||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
|
||||
{
|
||||
$imageName = $uploadedFile->getClientOriginalName();
|
||||
$imageData = file_get_contents($uploadedFile->getRealPath());
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,13 +75,14 @@ class ImageService
|
||||
* @param string $imageName
|
||||
* @param string $imageData
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
private function saveNew($imageName, $imageData, $type)
|
||||
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$secureUploads = Setting::get('app-secure-images');
|
||||
$secureUploads = setting('app-secure-images');
|
||||
$imageName = str_replace(' ', '-', $imageName);
|
||||
|
||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
@@ -92,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.');
|
||||
}
|
||||
@@ -100,7 +104,8 @@ class ImageService
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
'type' => $type
|
||||
'type' => $type,
|
||||
'uploaded_to' => $uploadedTo
|
||||
];
|
||||
|
||||
if (auth()->user() && auth()->user()->id !== 0) {
|
||||
@@ -163,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);
|
||||
@@ -253,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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,28 +44,39 @@ class SettingService
|
||||
|
||||
/**
|
||||
* Gets a setting value from the cache or database.
|
||||
* Looks at the system defaults if not cached or in database.
|
||||
* @param $key
|
||||
* @param $default
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getValueFromStore($key, $default)
|
||||
{
|
||||
// Check for an overriding value
|
||||
$overrideValue = $this->getOverrideValue($key);
|
||||
if ($overrideValue !== null) return $overrideValue;
|
||||
|
||||
// Check the cache
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
if ($this->cache->has($cacheKey)) {
|
||||
return $this->cache->get($cacheKey);
|
||||
}
|
||||
|
||||
// Check the database
|
||||
$settingObject = $this->getSettingObjectByKey($key);
|
||||
|
||||
if ($settingObject !== null) {
|
||||
$value = $settingObject->value;
|
||||
$this->cache->forever($cacheKey, $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Check the defaults set in the app config.
|
||||
$configPrefix = 'setting-defaults.' . $key;
|
||||
if (config()->has($configPrefix)) {
|
||||
$value = config($configPrefix);
|
||||
$this->cache->forever($cacheKey, $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,28 +113,29 @@ 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());
|
||||
}
|
||||
|
||||
// Otherwise let the user know this social account is not used by anyone.
|
||||
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
|
||||
if (\Setting::get('registration-enabled')) {
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Entity;
|
||||
use BookStack\View;
|
||||
|
||||
@@ -9,15 +8,18 @@ class ViewService
|
||||
|
||||
protected $view;
|
||||
protected $user;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param $view
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(View $view)
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +29,7 @@ class ViewService
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
if($this->user === null) return 0;
|
||||
if ($this->user === null) return 0;
|
||||
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
|
||||
// Add view if model exists
|
||||
if ($view) {
|
||||
@@ -44,52 +46,51 @@ class ViewService
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false $filterModel
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false|array $filterModel
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
$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));
|
||||
};
|
||||
|
||||
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable()->getResults();
|
||||
});
|
||||
return $viewedEntities;
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recently viewed entities for the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param Entity|bool $filterModel
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
if($this->user === null) return collect();
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->view->where('user_id', '=', auth()->user()->id);
|
||||
if ($this->user === null) return collect();
|
||||
|
||||
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable()->getResults();
|
||||
});
|
||||
return $viewedEntities;
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('user_id', '=', auth()->user()->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||
return $viewables;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset all view counts by deleting all views.
|
||||
*/
|
||||
@@ -98,5 +99,4 @@ class ViewService
|
||||
$this->view->truncate();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
91
app/User.php
91
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;
|
||||
@@ -14,21 +11,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* The database table used by the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'image_id'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
@@ -50,33 +44,38 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions and roles
|
||||
*/
|
||||
|
||||
/**
|
||||
* The roles that belong to the user.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
}
|
||||
|
||||
public function getRoleAttribute()
|
||||
{
|
||||
return $this->roles()->with('permissions')->first();
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user's permissions from their role.
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
private function loadPermissions()
|
||||
public function hasRole($role)
|
||||
{
|
||||
if (isset($this->permissions)) return;
|
||||
return $this->roles->pluck('name')->contains($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions belonging to a the current user.
|
||||
* @param bool $cache
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
|
||||
*/
|
||||
public function permissions($cache = true)
|
||||
{
|
||||
if(isset($this->permissions) && $cache) return $this->permissions;
|
||||
$this->load('roles.permissions');
|
||||
$permissions = $this->roles[0]->permissions;
|
||||
$permissionsArray = $permissions->pluck('name')->all();
|
||||
$this->permissions = $permissionsArray;
|
||||
$permissions = $this->roles->map(function($role) {
|
||||
return $role->permissions;
|
||||
})->flatten()->unique();
|
||||
$this->permissions = $permissions;
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,11 +85,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function can($permissionName)
|
||||
{
|
||||
if ($this->email == 'guest') {
|
||||
return false;
|
||||
}
|
||||
$this->loadPermissions();
|
||||
return array_search($permissionName, $this->permissions) !== false;
|
||||
if ($this->email === 'guest') return false;
|
||||
return $this->permissions()->pluck('name')->contains($permissionName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,17 +104,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function attachRoleId($id)
|
||||
{
|
||||
$this->roles()->sync([$id]);
|
||||
$this->roles()->attach($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the social account associated with this user.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function socialAccounts()
|
||||
{
|
||||
return $this->hasMany('BookStack\SocialAccount');
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,15 +133,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Returns the user's avatar,
|
||||
* Uses Gravatar as the avatar service.
|
||||
*
|
||||
* @param int $size
|
||||
* @return string
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +157,30 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shortened version of the user's name.
|
||||
* @param int $chars
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
{
|
||||
if (strlen($this->name) <= $chars) return $this->name;
|
||||
|
||||
$splitName = explode(' ', $this->name);
|
||||
if (strlen($splitName[0]) <= $chars) return $splitName[0];
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
160
app/helpers.php
160
app/helpers.php
@@ -1,30 +1,142 @@
|
||||
<?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 jointPermissions are checked against
|
||||
* that particular item.
|
||||
* @param $permission
|
||||
* @param Ownable $ownable
|
||||
* @return mixed
|
||||
*/
|
||||
function userCan($permission, Ownable $ownable = null)
|
||||
{
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissionService = app(\BookStack\Services\PermissionService::class);
|
||||
return $permissionService->checkOwnableUserAccess($ownable, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to access system settings.
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return mixed
|
||||
*/
|
||||
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 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';
|
||||
|
||||
/*
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"barryvdh/laravel-debugbar": "^2.0",
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "0.6.*"
|
||||
"barryvdh/laravel-dompdf": "0.6.*",
|
||||
"predis/predis": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
@@ -28,10 +29,7 @@
|
||||
],
|
||||
"psr-4": {
|
||||
"BookStack\\": "app/"
|
||||
},
|
||||
"files": [
|
||||
"app/helpers.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
|
||||
426
composer.lock
generated
426
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@ return [
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
'editor' => env('APP_EDITOR', 'html'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
@@ -29,7 +31,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -128,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,
|
||||
@@ -151,6 +152,8 @@ return [
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
BookStack\Providers\PaginationServiceProvider::class,
|
||||
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<?php
|
||||
|
||||
// MEMCACHED - Split out configuration into an array
|
||||
if (env('CACHE_DRIVER') === 'memcached') {
|
||||
$memcachedServerKeys = ['host', 'port', 'weight'];
|
||||
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
|
||||
foreach ($memcachedServers as $index => $memcachedServer) {
|
||||
$memcachedServerDetails = explode(':', $memcachedServer);
|
||||
if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
|
||||
if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
|
||||
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -49,11 +61,7 @@ return [
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'servers' => [
|
||||
[
|
||||
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
|
||||
],
|
||||
],
|
||||
'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
@@ -74,6 +82,6 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => 'laravel',
|
||||
'prefix' => env('CACHE_PREFIX', 'bookstack'),
|
||||
|
||||
];
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
<?php
|
||||
|
||||
// REDIS - Split out configuration into an array
|
||||
if (env('REDIS_SERVERS', false)) {
|
||||
$redisServerKeys = ['host', 'port', 'database'];
|
||||
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
|
||||
$redisConfig = [
|
||||
'cluster' => env('REDIS_CLUSTER', false)
|
||||
];
|
||||
foreach ($redisServers as $index => $redisServer) {
|
||||
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
|
||||
$redisServerDetails = explode(':', $redisServer);
|
||||
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
|
||||
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
|
||||
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -68,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' => '',
|
||||
@@ -123,16 +139,6 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'cluster' => false,
|
||||
|
||||
'default' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'database' => 0,
|
||||
],
|
||||
|
||||
],
|
||||
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
|
||||
|
||||
];
|
||||
|
||||
13
config/setting-defaults.php
Normal file
13
config/setting-defaults.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* The defaults for the system settings that are saved in the database.
|
||||
*/
|
||||
return [
|
||||
|
||||
'app-name' => 'BookStack',
|
||||
'app-editor' => 'wysiwyg',
|
||||
'app-color' => '#0288D1',
|
||||
'app-color-light' => 'rgba(21, 101, 192, 0.15)'
|
||||
|
||||
];
|
||||
@@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) {
|
||||
'email' => $faker->email,
|
||||
'password' => str_random(10),
|
||||
'remember_token' => str_random(10),
|
||||
'email_confirmed' => 1
|
||||
];
|
||||
});
|
||||
|
||||
@@ -45,3 +46,17 @@ $factory->define(BookStack\Page::class, function ($faker) {
|
||||
'text' => strip_tags($html)
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Role::class, function ($faker) {
|
||||
return [
|
||||
'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
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdatePermissionsAndRoles extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
|
||||
$editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
|
||||
|
||||
// Delete old permissions
|
||||
$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 Permissions',
|
||||
'restrictions-manage-own' => 'Manage Entity Permissions On Own Content'
|
||||
];
|
||||
foreach ($permissionsToCreate as $name => $displayName) {
|
||||
$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
|
||||
$entities = ['Book', 'Page', 'Chapter', 'Image'];
|
||||
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$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
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
|
||||
|
||||
// Delete old permissions
|
||||
$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) {
|
||||
$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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create admin permissions
|
||||
$entities = ['Settings', 'User'];
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$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,73 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddEntityAccessControls extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('images', function (Blueprint $table) {
|
||||
$table->integer('uploaded_to')->default(0);
|
||||
$table->index('uploaded_to');
|
||||
});
|
||||
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::create('restrictions', function(Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('restrictable_id');
|
||||
$table->string('restrictable_type');
|
||||
$table->integer('role_id');
|
||||
$table->string('action');
|
||||
$table->index('role_id');
|
||||
$table->index('action');
|
||||
$table->index(['restrictable_id', 'restrictable_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('images', function (Blueprint $table) {
|
||||
$table->dropColumn('uploaded_to');
|
||||
});
|
||||
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->dropColumn('restricted');
|
||||
});
|
||||
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->dropColumn('restricted');
|
||||
});
|
||||
|
||||
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('restricted');
|
||||
});
|
||||
|
||||
Schema::drop('restrictions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddPageRevisionTypes extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->string('type')->default('version');
|
||||
$table->index('type');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->dropColumn('type');
|
||||
});
|
||||
}
|
||||
}
|
||||
32
database/migrations/2016_03_13_082138_add_page_drafts.php
Normal file
32
database/migrations/2016_03_13_082138_add_page_drafts.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddPageDrafts extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('pages', function(Blueprint $table) {
|
||||
$table->boolean('draft')->default(false);
|
||||
$table->index('draft');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('draft');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddMarkdownSupport extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->longText('markdown')->default('');
|
||||
});
|
||||
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->longText('markdown')->default('');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('markdown');
|
||||
});
|
||||
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->dropColumn('markdown');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
|
||||
public function run()
|
||||
{
|
||||
$user = factory(BookStack\User::class, 1)->create();
|
||||
$role = \BookStack\Role::getDefault();
|
||||
$role = \BookStack\Role::getRole('editor');
|
||||
$user->attachRole($role);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
13
package.json
13
package.json
@@ -4,14 +4,17 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,17 +21,19 @@
|
||||
</filter>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_DEBUG" value="false"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<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=a50dcfa",
|
||||
"css/print-styles.css": "css/print-styles.css?version=a50dcfa",
|
||||
"js/common.js": "js/common.js?version=a50dcfa"
|
||||
"css/styles.css": "css/styles.css?version=ba2edb5",
|
||||
"css/print-styles.css": "css/print-styles.css?version=ba2edb5",
|
||||
"js/common.js": "js/common.js?version=ba2edb5"
|
||||
}
|
||||
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
Binary file not shown.
@@ -1,675 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata></metadata>
|
||||
<defs>
|
||||
<font id="robotobold" horiz-adv-x="1175" >
|
||||
<font-face units-per-em="2048" ascent="1536" descent="-512" />
|
||||
<missing-glyph horiz-adv-x="510" />
|
||||
<glyph unicode="fi" horiz-adv-x="1246" d="M20 870v212h161v50q2 204 125.5 314.5t348.5 110.5q135 0 328 -59l-42 -239q-98 29 -152 37.5t-116 8.5q-202 0 -202 -179v-44h213v-212h-213v-870h-290v870h-161zM828 0v1082h290v-1082h-290z" />
|
||||
<glyph unicode="fl" horiz-adv-x="1255" d="M29 870v212h161v84q2 188 118 289.5t328 101.5q151 0 490 -32v-1525h-290v1312q-86 10 -160 10q-196 0 -196 -167v-73h215v-212h-215v-870h-290v870h-161z" />
|
||||
<glyph horiz-adv-x="0" />
|
||||
<glyph horiz-adv-x="0" />
|
||||
<glyph unicode="
" horiz-adv-x="510" />
|
||||
<glyph horiz-adv-x="0" />
|
||||
<glyph unicode=" " horiz-adv-x="510" />
|
||||
<glyph unicode="	" horiz-adv-x="510" />
|
||||
<glyph unicode=" " horiz-adv-x="510" />
|
||||
<glyph unicode="!" horiz-adv-x="557" d="M124 136q0 66 44.5 109.5t116.5 43.5q73 0 117.5 -43t44.5 -110q0 -66 -44.5 -109t-117.5 -43q-72 0 -116.5 43t-44.5 109zM131 1456h308l-34 -1009h-240z" />
|
||||
<glyph unicode=""" horiz-adv-x="657" d="M64 987v549h202v-138l-40 -411h-162zM390 987v549h201v-138l-39 -411h-162z" />
|
||||
<glyph unicode="#" horiz-adv-x="1220" d="M64 410v172h257l50 284h-232v174h263l73 416h182l-73 -416h194l73 416h183l-73 -416h219v-174h-250l-50 -284h224v-172h-254l-72 -410h-183l72 410h-193l-72 -410h-183l72 410h-227zM504 582h193l50 284h-194z" />
|
||||
<glyph unicode="$" d="M92 457h289q0 -119 56.5 -182.5t162.5 -63.5q88 0 139 46.5t51 125.5q0 82 -45.5 130.5t-154.5 89.5t-188 81.5t-135.5 93t-88 123.5t-31.5 169q0 169 108 277t287 126v214h160v-217q177 -25 277 -147.5t100 -317.5h-289q0 120 -49.5 179.5t-132.5 59.5q-82 0 -127 -46.5 t-45 -128.5q0 -76 44 -122t163.5 -94t196.5 -90.5t130 -96t81 -122t28 -159.5q0 -170 -106 -276t-292 -125v-199h-159v198q-205 22 -317.5 145.5t-112.5 328.5z" />
|
||||
<glyph unicode="%" horiz-adv-x="1512" d="M95 1105v71q0 134 87 217.5t228 83.5q143 0 230 -82.5t87 -223.5v-72q0 -135 -87 -217t-228 -82q-142 0 -229.5 82.5t-87.5 222.5zM287 1099q0 -60 34.5 -96.5t90.5 -36.5t89 37t33 99v74q0 60 -33 97t-91 37q-55 0 -89 -36.5t-34 -101.5v-73zM328 185l711 1138l141 -76 l-711 -1138zM791 283v74q0 135 88 217.5t228 82.5q142 0 229.5 -81.5t87.5 -224.5v-72q0 -134 -86 -216.5t-229 -82.5q-144 0 -231 83t-87 220zM983 279q0 -55 36 -94t90 -39q122 0 122 135v76q0 60 -34 96.5t-90 36.5t-90 -36.5t-34 -99.5v-75z" />
|
||||
<glyph unicode="&" horiz-adv-x="1344" d="M71 392q0 101 56.5 187t207.5 191q-65 87 -102 163.5t-37 159.5q0 170 107.5 276.5t289.5 106.5q163 0 267.5 -97t104.5 -242q0 -174 -176 -307l-112 -81l251 -292q59 116 59 256h246q0 -284 -131 -460l218 -253h-328l-75 86q-161 -106 -370 -106q-216 0 -346 113.5 t-130 298.5zM361 408q0 -87 55.5 -141.5t144.5 -54.5q104 0 197 58l-287 332l-21 -15q-89 -76 -89 -179zM455 1097q0 -75 88 -189l77 51q59 38 81.5 74t22.5 86t-37 87t-95 37q-62 0 -99.5 -40t-37.5 -106z" />
|
||||
<glyph unicode="'" horiz-adv-x="331" d="M63 985v551h212v-147l-29 -404h-183z" />
|
||||
<glyph unicode="(" horiz-adv-x="719" d="M124 539v53q0 229 61 436t180 366.5t258 221.5l56 -156q-146 -108 -228 -334t-82 -528v-31q0 -303 81 -530t229 -339l-56 -153q-136 61 -253.5 215.5t-179.5 356.5t-66 422z" />
|
||||
<glyph unicode=")" horiz-adv-x="722" d="M40 -302q142 107 224 330t85 513v53q0 299 -81.5 527t-227.5 342l56 153q136 -59 256.5 -214t187 -361t69.5 -427v-45q0 -226 -65 -435t-187 -369t-261 -220z" />
|
||||
<glyph unicode="*" horiz-adv-x="928" d="M27 1051l59 181l311 -125l-20 349h196l-20 -356l303 123l59 -183l-320 -89l210 -266l-159 -113l-182 292l-180 -282l-159 108l216 272z" />
|
||||
<glyph unicode="+" horiz-adv-x="1118" d="M57 554v261h362v391h275v-391h361v-261h-361v-408h-275v408h-362z" />
|
||||
<glyph unicode="," horiz-adv-x="500" d="M35 -286l36 65q67 122 69 243v224h244l-1 -200q-1 -111 -56 -224t-141 -187z" />
|
||||
<glyph unicode="-" horiz-adv-x="794" d="M110 507v233h563v-233h-563z" />
|
||||
<glyph unicode="." horiz-adv-x="595" d="M126 142q0 69 46.5 112t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5z" />
|
||||
<glyph unicode="/" horiz-adv-x="765" d="M-13 -125l536 1581h215l-536 -1581h-215z" />
|
||||
<glyph unicode="0" d="M95 587v268q0 305 126.5 463t364.5 158t364 -156.5t129 -449.5v-268q0 -302 -125 -462t-366 -160q-238 0 -364 157t-129 450zM384 564q0 -178 48.5 -264.5t155.5 -86.5q106 0 153 83t49 254v346q0 181 -49.5 263.5t-154.5 82.5q-102 0 -150.5 -78.5t-51.5 -245.5v-354z " />
|
||||
<glyph unicode="1" d="M167 1007v235l603 216h31v-1458h-289v1114z" />
|
||||
<glyph unicode="2" d="M70 998q0 133 63.5 243t179.5 172.5t263 62.5q225 0 349.5 -108t124.5 -305q0 -108 -56 -220t-192 -261l-331 -349h626v-233h-998v198l471 502q97 106 143.5 185t46.5 150q0 97 -49 152.5t-140 55.5q-98 0 -154.5 -67.5t-56.5 -177.5h-290z" />
|
||||
<glyph unicode="3" d="M64 399h289q0 -82 61.5 -134t151.5 -52q103 0 161.5 54.5t58.5 144.5q0 218 -240 218h-153v226h154q110 0 163 55t53 146q0 88 -52.5 137t-144.5 49q-83 0 -139 -45.5t-56 -118.5h-289q0 114 61.5 204.5t172 141.5t243.5 51q231 0 362 -110.5t131 -304.5q0 -100 -61 -184 t-160 -129q123 -44 183.5 -132t60.5 -208q0 -194 -141.5 -311t-374.5 -117q-218 0 -356.5 115t-138.5 304z" />
|
||||
<glyph unicode="4" d="M55 497l607 959h292v-908h165v-233h-165v-315h-289v315h-597zM343 548h322v514l-19 -33z" />
|
||||
<glyph unicode="5" d="M105 405h286q9 -91 63.5 -141.5t142.5 -50.5q98 0 151 70.5t53 199.5q0 124 -61 190t-173 66q-103 0 -167 -54l-28 -26l-230 57l84 740h816v-241h-579l-36 -313q103 55 219 55q208 0 326 -129t118 -361q0 -141 -59.5 -252.5t-170.5 -173t-262 -61.5q-132 0 -245 53.5 t-178.5 150.5t-69.5 221z" />
|
||||
<glyph unicode="6" d="M100 567v104q0 237 89.5 418.5t257 281t388.5 100.5h48v-238h-28q-196 -3 -315.5 -102t-143.5 -275q116 118 293 118q190 0 302 -136t112 -358q0 -142 -61.5 -257t-174 -179t-254.5 -64q-230 0 -371.5 160t-141.5 427zM390 521q0 -145 57 -226.5t160 -81.5 q93 0 150.5 73.5t57.5 190.5q0 119 -58 192t-156 73q-70 0 -127 -36.5t-84 -96.5v-88z" />
|
||||
<glyph unicode="7" d="M61 1222v234h1028v-162l-563 -1294h-305l564 1222h-724z" />
|
||||
<glyph unicode="8" d="M95 399q0 121 62 211t167 140q-93 49 -145.5 131t-52.5 188q0 186 124 296.5t337 110.5q212 0 336.5 -109.5t124.5 -297.5q0 -106 -53 -188t-146 -131q106 -51 168 -140.5t62 -210.5q0 -194 -132 -306.5t-359 -112.5t-360 113t-133 306zM384 420q0 -93 54 -150t150 -57 q94 0 147.5 55t53.5 152q0 95 -55 152t-148 57q-92 0 -147 -56.5t-55 -152.5zM416 1055q0 -87 45 -140t127 -53t126.5 53t44.5 140q0 85 -45 136.5t-127 51.5q-81 0 -126 -50t-45 -138z" />
|
||||
<glyph unicode="9" d="M86 961q0 143 62.5 262.5t174.5 186t252 66.5q144 0 256 -72t174 -207t63 -309v-107q0 -364 -181 -572t-513 -222l-71 -1v241l64 1q377 17 408 354q-113 -111 -264 -111q-193 0 -309 132.5t-116 357.5zM374 964q0 -119 54.5 -195.5t153.5 -76.5q70 0 120.5 36t76.5 87 v119q0 147 -56 228t-150 81q-87 0 -143 -79.5t-56 -199.5z" />
|
||||
<glyph unicode=":" horiz-adv-x="578" d="M125 142q0 69 46.5 112t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5zM125 961q0 69 46.5 112t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5z" />
|
||||
<glyph unicode=";" horiz-adv-x="537" d="M57 -286l36 65q67 122 69 243v224h244l-1 -200q-1 -111 -56 -224t-141 -187zM108 961q0 69 46.5 112t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5z" />
|
||||
<glyph unicode="<" horiz-adv-x="1042" d="M54 502v236l861 365v-280l-570 -205l570 -201v-280z" />
|
||||
<glyph unicode="=" horiz-adv-x="1172" d="M136 313v236h894v-236h-894zM136 746v236h894v-236h-894z" />
|
||||
<glyph unicode=">" horiz-adv-x="1058" d="M120 136v279l581 206l-581 203v278l871 -365v-235z" />
|
||||
<glyph unicode="?" horiz-adv-x="1019" d="M45 1069q2 191 123.5 299t331.5 108q212 0 329 -102.5t117 -289.5q0 -85 -38 -160.5t-133 -167.5l-81 -77q-76 -73 -87 -171l-4 -61h-256q0 140 34 223t124.5 163.5t120.5 131t30 106.5q0 169 -156 169q-74 0 -118.5 -45.5t-46.5 -125.5h-290zM318 140q0 67 45.5 110.5 t116.5 43.5t116.5 -43.5t45.5 -110.5q0 -66 -44.5 -109t-117.5 -43t-117.5 43t-44.5 109z" />
|
||||
<glyph unicode="@" horiz-adv-x="1833" d="M87 463q12 276 126 495t310.5 338.5t443.5 119.5q251 0 432 -107.5t271 -307.5t79 -465q-11 -256 -126.5 -406.5t-310.5 -150.5q-86 0 -148.5 37t-94.5 106q-100 -140 -261 -140q-146 0 -226 123t-60 325q18 165 83.5 293.5t165.5 197.5t216 69q143 0 244 -66l63 -43 l-51 -578q-10 -79 17.5 -121t87.5 -42q92 0 154 107.5t68 281.5q17 349 -140.5 536.5t-466.5 187.5q-193 0 -344 -98t-238 -278.5t-98 -413.5q-16 -354 142 -547.5t473 -193.5q83 0 174 18.5t157 49.5l38 -154q-61 -40 -164.5 -64.5t-208.5 -24.5q-264 0 -450 106.5 t-277.5 314t-79.5 495.5zM744 430q-11 -132 23 -200.5t110 -68.5q49 0 93 43t73 124l42 473q-39 13 -80 13q-115 0 -178.5 -98t-82.5 -286z" />
|
||||
<glyph unicode="A" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM507 543h364l-183 545z" />
|
||||
<glyph unicode="B" horiz-adv-x="1307" d="M130 0v1456h510q265 0 402 -101.5t137 -297.5q0 -107 -55 -188.5t-153 -119.5q112 -28 176.5 -113t64.5 -208q0 -210 -134 -318t-382 -110h-566zM430 241h257q106 0 165.5 50.5t59.5 139.5q0 200 -207 203h-275v-393zM430 846h222q227 4 227 181q0 99 -57.5 142.5 t-181.5 43.5h-210v-367z" />
|
||||
<glyph unicode="C" horiz-adv-x="1340" d="M86 686v89q0 210 74 370t211.5 245.5t319.5 85.5q252 0 406 -135t178 -379h-300q-11 141 -78.5 204.5t-205.5 63.5q-150 0 -224.5 -107.5t-76.5 -333.5v-110q0 -236 71.5 -345t225.5 -109q139 0 207.5 63.5t78.5 196.5h300q-17 -235 -173.5 -370t-412.5 -135 q-280 0 -440.5 188.5t-160.5 517.5z" />
|
||||
<glyph unicode="D" horiz-adv-x="1331" d="M130 0v1456h448q192 0 343.5 -86.5t236.5 -246t85 -362.5v-67q0 -203 -83.5 -361t-235.5 -245t-343 -88h-451zM430 241h145q176 0 269 115t95 329v77q0 222 -92 336.5t-269 114.5h-148v-972z" />
|
||||
<glyph unicode="E" horiz-adv-x="1152" d="M130 0v1456h974v-243h-674v-347h576v-235h-576v-390h676v-241h-976z" />
|
||||
<glyph unicode="F" horiz-adv-x="1122" d="M130 0v1456h948v-243h-648v-376h576v-242h-576v-595h-300z" />
|
||||
<glyph unicode="G" horiz-adv-x="1395" d="M94 671v99q0 218 73.5 377.5t212 244t324.5 84.5q259 0 405 -123.5t173 -359.5h-292q-20 125 -88.5 183t-188.5 58q-153 0 -233 -115t-81 -342v-93q0 -229 87 -346t255 -117q169 0 241 72v251h-273v221h573v-581q-81 -97 -229 -150.5t-328 -53.5q-189 0 -331.5 82.5 t-220 239.5t-79.5 369z" />
|
||||
<glyph unicode="H" horiz-adv-x="1447" d="M130 0v1456h300v-590h585v590h300v-1456h-300v624h-585v-624h-300z" />
|
||||
<glyph unicode="I" horiz-adv-x="597" d="M149 0v1456h300v-1456h-300z" />
|
||||
<glyph unicode="J" horiz-adv-x="1144" d="M40 430h302q0 -107 45 -158t142 -51q86 0 137 59t51 168v1008h300v-1008q0 -139 -61.5 -245.5t-173.5 -164.5t-253 -58q-231 0 -360 117.5t-129 332.5z" />
|
||||
<glyph unicode="K" horiz-adv-x="1300" d="M130 0v1456h300v-660l132 181l371 479h369l-517 -647l532 -809h-357l-374 584l-156 -168v-416h-300z" />
|
||||
<glyph unicode="L" horiz-adv-x="1109" d="M130 0v1456h300v-1215h637v-241h-937z" />
|
||||
<glyph unicode="M" horiz-adv-x="1794" d="M130 0v1456h392l374 -1056l372 1056h394v-1456h-301v398l30 687l-393 -1085h-206l-392 1084l30 -686v-398h-300z" />
|
||||
<glyph unicode="N" horiz-adv-x="1446" d="M130 0v1456h300l585 -960v960h299v-1456h-300l-584 958v-958h-300z" />
|
||||
<glyph unicode="O" horiz-adv-x="1414" d="M86 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -215 -76 -377t-217.5 -250t-324.5 -88q-181 0 -323 87t-220 248.5t-79 371.5zM390 695q0 -223 82 -346t236 -123q151 0 232 118.5t82 345.5v71q0 229 -82 348t-234 119 q-151 0 -233 -117.5t-83 -344.5v-71z" />
|
||||
<glyph unicode="P" horiz-adv-x="1321" d="M130 0v1456h568q164 0 288.5 -60t191.5 -170.5t67 -251.5q0 -214 -146.5 -337.5t-405.5 -123.5h-263v-513h-300zM430 756h268q119 0 181.5 56t62.5 160q0 107 -63 173t-174 68h-275v-457z" />
|
||||
<glyph unicode="Q" horiz-adv-x="1414" d="M84 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -204 -66 -354.5t-183 -241.5l242 -190l-191 -169l-310 249q-53 -9 -110 -9q-181 0 -323 87t-220 248.5t-79 371.5zM388 695q0 -223 82 -346t236 -123q151 0 232 118.5t82 345.5 v71q0 229 -82 348t-234 119q-151 0 -233 -117.5t-83 -344.5v-71z" />
|
||||
<glyph unicode="R" horiz-adv-x="1307" d="M130 0v1456h541q258 0 398 -115t140 -325q0 -149 -64.5 -248.5t-195.5 -158.5l315 -595v-14h-322l-273 533h-239v-533h-300zM430 776h242q113 0 175 57.5t62 158.5q0 103 -58.5 162t-179.5 59h-241v-437z" />
|
||||
<glyph unicode="S" horiz-adv-x="1259" d="M69 458h301q0 -241 288 -241q107 0 167 43.5t60 121.5q0 85 -60 130.5t-216 96t-247 99.5q-248 134 -248 361q0 118 66.5 210.5t191 144.5t279.5 52q156 0 278 -56.5t189.5 -159.5t67.5 -234h-300q0 100 -63 155.5t-177 55.5q-110 0 -171 -46.5t-61 -122.5 q0 -71 71.5 -119t210.5 -90q256 -77 373 -191t117 -284q0 -189 -143 -296.5t-385 -107.5q-168 0 -306 61.5t-210.5 168.5t-72.5 248z" />
|
||||
<glyph unicode="T" horiz-adv-x="1267" d="M40 1213v243h1186v-243h-446v-1213h-300v1213h-440z" />
|
||||
<glyph unicode="U" horiz-adv-x="1348" d="M116 486v970h300v-961q0 -143 68.5 -208.5t189.5 -65.5q253 0 257 266v969h301v-959q0 -239 -149.5 -378t-408.5 -139q-255 0 -405 135t-153 371z" />
|
||||
<glyph unicode="V" horiz-adv-x="1339" d="M7 1456h333l328 -1095l330 1095h334l-507 -1456h-313z" />
|
||||
<glyph unicode="W" horiz-adv-x="1791" d="M35 1456h299l197 -1034l240 1034h254l239 -1036l196 1036h299l-323 -1456h-302l-237 974l-237 -974h-302z" />
|
||||
<glyph unicode="X" horiz-adv-x="1301" d="M22 0l435 734l-424 722h345l273 -502l273 502h345l-424 -722l435 -734h-349l-280 510l-280 -510h-349z" />
|
||||
<glyph unicode="Y" horiz-adv-x="1266" d="M2 1456h329l301 -656l303 656h328l-478 -928v-528h-305v528z" />
|
||||
<glyph unicode="Z" horiz-adv-x="1241" d="M73 0v176l720 1037h-719v243h1092v-172l-718 -1043h734v-241h-1109z" />
|
||||
<glyph unicode="[" horiz-adv-x="569" d="M120 -339v2033h432v-223h-142v-1587h142v-223h-432z" />
|
||||
<glyph unicode="\" horiz-adv-x="864" d="M0 1456h295l608 -1581h-296z" />
|
||||
<glyph unicode="]" horiz-adv-x="569" d="M13 -116h143v1587h-143v223h432v-2033h-432v223z" />
|
||||
<glyph unicode="^" horiz-adv-x="895" d="M44 729l299 727h210l299 -727h-229l-175 457l-174 -457h-230z" />
|
||||
<glyph unicode="_" horiz-adv-x="914" d="M1 0h911v-226h-911v226z" />
|
||||
<glyph unicode="`" horiz-adv-x="677" d="M52 1536h315l198 -310h-237z" />
|
||||
<glyph unicode="a" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M357 325q0 -54 38 -89t104 -35q64 0 118 28.5t80 76.5v186h-108q-217 0 -231 -150z" />
|
||||
<glyph unicode="b" horiz-adv-x="1153" d="M111 0v1536h289v-551q100 117 263 117q198 0 310.5 -145.5t112.5 -409.5v-16q0 -260 -111 -405.5t-310 -145.5q-176 0 -281 135l-13 -115h-260zM400 327q53 -114 192 -114q140 0 184 138q21 66 21 201q0 164 -52 239.5t-155 75.5q-138 0 -190 -113v-427z" />
|
||||
<glyph unicode="c" horiz-adv-x="1068" d="M66 535v19q0 250 133 399t365 149q203 0 325.5 -115.5t124.5 -307.5h-271q-2 84 -52 136.5t-132 52.5q-101 0 -152.5 -73.5t-51.5 -238.5v-30q0 -167 51 -240t155 -73q80 0 130 44t52 117h271q-1 -110 -60 -201.5t-161.5 -142t-226.5 -50.5q-232 0 -366 147.5t-134 407.5 z" />
|
||||
<glyph unicode="d" horiz-adv-x="1154" d="M66 549q0 253 113.5 403t310.5 150q158 0 261 -118v552h290v-1536h-261l-14 115q-108 -135 -278 -135q-191 0 -306.5 150.5t-115.5 418.5zM355 528q0 -152 53 -233t154 -81q134 0 189 113v427q-54 113 -187 113q-209 0 -209 -339z" />
|
||||
<glyph unicode="e" horiz-adv-x="1107" d="M72 515v28q0 163 63 291.5t178.5 198t263.5 69.5q222 0 349.5 -140t127.5 -397v-118h-689q14 -106 84.5 -170t178.5 -64q167 0 261 121l142 -159q-65 -92 -176 -143.5t-246 -51.5q-238 0 -387.5 146t-149.5 389zM368 644h402v23q-2 96 -52 148.5t-142 52.5 q-86 0 -139.5 -58t-68.5 -166z" />
|
||||
<glyph unicode="f" horiz-adv-x="734" d="M29 870v212h161v92q0 182 104.5 282.5t292.5 100.5q60 0 147 -20l-3 -224q-36 9 -88 9q-163 0 -163 -153v-87h215v-212h-215v-870h-290v870h-161z" />
|
||||
<glyph unicode="g" horiz-adv-x="1169" d="M69 537v12q0 249 118.5 401t319.5 152q178 0 277 -122l12 102h262v-1046q0 -142 -64.5 -247t-181.5 -160t-274 -55q-119 0 -232 47.5t-171 122.5l128 176q108 -121 262 -121q115 0 179 61.5t64 174.5v58q-100 -113 -263 -113q-195 0 -315.5 152.5t-120.5 404.5zM358 528 q0 -147 59 -230.5t162 -83.5q132 0 189 99v455q-58 99 -187 99q-104 0 -163.5 -85t-59.5 -254z" />
|
||||
<glyph unicode="h" horiz-adv-x="1146" d="M104 0v1536h289v-572q115 138 289 138q352 0 357 -409v-693h-289v685q0 93 -40 137.5t-133 44.5q-127 0 -184 -98v-769h-289z" />
|
||||
<glyph unicode="i" horiz-adv-x="543" d="M109 1362q0 65 43.5 107t118.5 42q74 0 118 -42t44 -107q0 -66 -44.5 -108t-117.5 -42t-117.5 42t-44.5 108zM126 0v1082h290v-1082h-290z" />
|
||||
<glyph unicode="j" horiz-adv-x="532" d="M-95 -191q52 -9 91 -9q131 0 131 139v1143h290v-1141q0 -179 -95 -278.5t-274 -99.5q-75 0 -143 17v229zM104 1362q0 65 43.5 107t118.5 42t118.5 -42t43.5 -107q0 -66 -44.5 -108t-117.5 -42t-117.5 42t-44.5 108z" />
|
||||
<glyph unicode="k" horiz-adv-x="1094" d="M111 0v1536h289v-851l56 72l277 325h347l-391 -451l425 -631h-332l-278 434l-104 -104v-330h-289z" />
|
||||
<glyph unicode="l" horiz-adv-x="543" d="M126 0v1536h290v-1536h-290z" />
|
||||
<glyph unicode="m" horiz-adv-x="1773" d="M111 0v1082h271l9 -121q115 141 311 141q209 0 287 -165q114 165 325 165q176 0 262 -102.5t86 -308.5v-691h-290v690q0 92 -36 134.5t-127 42.5q-130 0 -180 -124l1 -743h-289v689q0 94 -37 136t-126 42q-123 0 -178 -102v-765h-289z" />
|
||||
<glyph unicode="n" horiz-adv-x="1147" d="M105 0v1082h272l9 -125q116 145 311 145q172 0 256 -101t86 -302v-699h-289v692q0 92 -40 133.5t-133 41.5q-122 0 -183 -104v-763h-289z" />
|
||||
<glyph unicode="o" horiz-adv-x="1158" d="M66 538v13q0 161 62 287t178.5 195t270.5 69q219 0 357.5 -134t154.5 -364l2 -74q0 -249 -139 -399.5t-373 -150.5t-373.5 150t-139.5 408zM355 530q0 -154 58 -235.5t166 -81.5q105 0 164 80.5t59 257.5q0 151 -59 234t-166 83q-106 0 -164 -82.5t-58 -255.5z" />
|
||||
<glyph unicode="p" horiz-adv-x="1153" d="M111 -416v1498h268l10 -106q105 126 274 126q200 0 311 -148t111 -408v-15q0 -250 -113.5 -400.5t-306.5 -150.5q-164 0 -265 114v-510h-289zM400 320q53 -107 189 -107q207 0 207 339q0 151 -53.5 233t-155.5 82q-136 0 -187 -104v-443z" />
|
||||
<glyph unicode="q" horiz-adv-x="1157" d="M66 551q0 255 113.5 403t311.5 148q174 0 277 -133l19 113h254v-1498h-290v509q-100 -113 -262 -113q-193 0 -308 150t-115 421zM355 530q0 -155 54.5 -236t153.5 -81q133 0 188 106v447q-54 102 -186 102q-100 0 -155 -81t-55 -257z" />
|
||||
<glyph unicode="r" horiz-adv-x="747" d="M111 0v1082h273l8 -129q87 149 241 149q48 0 90 -13l-4 -278q-59 8 -104 8q-164 0 -215 -111v-708h-289z" />
|
||||
<glyph unicode="s" horiz-adv-x="1053" d="M56 344h274q4 -77 57 -118t142 -41q83 0 125.5 31.5t42.5 82.5q0 53 -52.5 83.5t-168.5 54.5q-386 81 -386 328q0 144 119.5 240.5t312.5 96.5q206 0 329.5 -97t123.5 -252h-289q0 62 -40 102.5t-125 40.5q-73 0 -113 -33t-40 -84q0 -48 45.5 -77.5t153.5 -51t182 -48.5 q229 -84 229 -291q0 -148 -127 -239.5t-328 -91.5q-136 0 -241.5 48.5t-165.5 133t-60 182.5z" />
|
||||
<glyph unicode="t" horiz-adv-x="692" d="M10 870v212h158v266h289v-266h185v-212h-185v-540q0 -60 23 -86t88 -26q48 0 85 7v-219q-85 -26 -175 -26q-304 0 -310 307v583h-158z" />
|
||||
<glyph unicode="u" horiz-adv-x="1146" d="M104 373v709h289v-699q0 -169 154 -169q147 0 202 102v766h290v-1082h-272l-8 110q-107 -130 -296 -130q-174 0 -265.5 100t-93.5 293z" />
|
||||
<glyph unicode="v" horiz-adv-x="1035" d="M13 1082h302l201 -729l201 729h302l-365 -1082h-276z" />
|
||||
<glyph unicode="w" horiz-adv-x="1505" d="M28 1082h279l141 -688l198 688h209l197 -689l142 689h279l-276 -1082h-242l-205 681l-205 -681h-241z" />
|
||||
<glyph unicode="x" horiz-adv-x="1042" d="M21 0l321 552l-307 530h310l178 -323l182 323h309l-308 -530l321 -552h-310l-193 340l-192 -340h-311z" />
|
||||
<glyph unicode="y" horiz-adv-x="1028" d="M3 1082h311l201 -673l200 673h310l-435 -1250l-24 -57q-97 -212 -320 -212q-63 0 -128 19v219l44 -1q82 0 122.5 25t63.5 83l34 89z" />
|
||||
<glyph unicode="z" horiz-adv-x="1042" d="M74 0v176l509 672h-494v234h867v-171l-513 -678h529v-233h-898z" />
|
||||
<glyph unicode="{" horiz-adv-x="676" d="M48 515v207q174 0 178 199v212q0 185 90 299t270 165l56 -161q-76 -28 -115 -97.5t-41 -192.5v-210q0 -226 -179 -317q179 -92 179 -319v-212q5 -234 156 -286l-56 -162q-360 101 -360 465v199q0 211 -178 211z" />
|
||||
<glyph unicode="|" horiz-adv-x="518" d="M173 -270v1726h175v-1726h-175z" />
|
||||
<glyph unicode="}" horiz-adv-x="676" d="M34 -198q152 54 156 290v212q0 226 183 314q-183 88 -183 319v209q-4 233 -156 290l56 161q179 -50 269 -163.5t91 -297.5v-215q4 -199 178 -199v-207q-178 0 -178 -209v-217q-8 -350 -360 -449z" />
|
||||
<glyph unicode="~" horiz-adv-x="1328" d="M106 415q0 186 90.5 299t240.5 113q78 0 142 -30t145.5 -107.5t148.5 -77.5q59 0 96.5 53t37.5 129l214 -1q0 -186 -93 -302t-240 -116q-74 0 -137.5 28.5t-146.5 108t-153 79.5q-58 0 -94 -50t-36 -128z" />
|
||||
<glyph unicode="¡" horiz-adv-x="578" d="M128 948q0 67 44.5 110t117.5 43t117.5 -43t44.5 -110t-46 -110t-116 -43t-116 43t-46 110zM137 -369l33 1008h241l33 -1008h-307z" />
|
||||
<glyph unicode="¢" horiz-adv-x="1178" d="M99 532v22q0 218 105 363t294 177v224h200v-225q163 -29 255.5 -140t94.5 -274h-272q-2 86 -52 137.5t-131 51.5q-102 0 -153 -74.5t-52 -234.5v-33q0 -168 51.5 -240.5t154.5 -72.5q80 0 130 44t52 117h272q-2 -145 -98.5 -250.5t-251.5 -134.5v-234h-200v233 q-187 30 -293 174t-106 370z" />
|
||||
<glyph unicode="£" horiz-adv-x="1217" d="M99 576v236h154l-7 227q0 202 123.5 319t330.5 117q212 0 333 -112.5t121 -304.5h-287q0 85 -43.5 130t-124.5 45q-66 0 -109.5 -49t-43.5 -145l9 -227h309v-236h-300l6 -139q0 -123 -62 -196h653v-241h-1059v241h92q72 18 72 179l-5 156h-162z" />
|
||||
<glyph unicode="¤" horiz-adv-x="1418" d="M81 118l135 137q-100 156 -100 353q0 204 109 365l-144 147l141 144l142 -145q155 115 348 115q194 0 349 -117l144 148l142 -145l-148 -151q107 -159 107 -361q0 -193 -98 -349l139 -141l-142 -145l-132 134q-159 -127 -361 -127q-203 0 -361 126l-129 -132zM302 608 q0 -118 54 -219.5t149.5 -160t206.5 -58.5q110 0 205.5 58.5t149.5 160t54 219.5q0 119 -54 219.5t-149 158.5t-206 58q-112 0 -207 -58t-149 -158.5t-54 -219.5z" />
|
||||
<glyph unicode="¥" horiz-adv-x="1254" d="M20 1456h330l276 -606l277 606h329l-375 -714h234v-175h-319v-115h319v-174h-319v-278h-300v278h-336v174h336v115h-336v175h260z" />
|
||||
<glyph unicode="¦" horiz-adv-x="516" d="M128 -270v795h260v-795h-260zM128 698v758h260v-758h-260z" />
|
||||
<glyph unicode="§" horiz-adv-x="1287" d="M92 -35l289 1q0 -89 64 -136.5t191 -47.5q112 0 170 37.5t58 100.5q0 65 -64.5 106t-247 91.5t-275 105.5t-138 130t-45.5 181q0 180 162 273q-136 103 -136 288q0 171 140.5 276t379.5 105q247 0 383 -113t136 -314h-289q0 87 -61.5 140.5t-168.5 53.5q-110 0 -170 -39 t-60 -107q0 -73 55.5 -110t240.5 -86t282.5 -104t144.5 -131.5t47 -184.5q0 -182 -162 -271q135 -102 135 -288q0 -175 -137.5 -274.5t-379.5 -99.5q-257 0 -400.5 107t-143.5 310zM383 563q0 -69 41.5 -106.5t165.5 -77.5l222 -67q82 47 82 140q0 62 -45.5 101.5 t-163.5 79.5l-227 71q-75 -42 -75 -141z" />
|
||||
<glyph unicode="¨" horiz-adv-x="956" d="M93 1365q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM580 1365q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="©" horiz-adv-x="1606" d="M86 729q0 202 93.5 375t259 272.5t357.5 99.5t357.5 -99.5t259 -272.5t93.5 -375q0 -204 -95 -377.5t-259.5 -272.5t-355.5 -99q-193 0 -357.5 100t-258.5 273t-94 376zM208 729q0 -170 77.5 -314t214 -227.5t296.5 -83.5t297.5 85t213.5 229t76 311q0 166 -75 308.5 t-212 228t-300 85.5q-159 0 -295.5 -82t-214.5 -226t-78 -314zM433 675v113q0 174 95.5 280.5t253.5 106.5q163 0 249.5 -82.5t86.5 -231.5h-156q0 96 -46 137.5t-134 41.5q-92 0 -142.5 -67.5t-51.5 -180.5v-123q0 -117 51 -184.5t143 -67.5q89 0 134 40.5t45 138.5h156 q0 -152 -87.5 -233t-247.5 -81t-254.5 106.5t-94.5 286.5z" />
|
||||
<glyph unicode="ª" horiz-adv-x="909" d="M137 919q0 110 84 170.5t257 60.5h102v51q0 127 -116 127q-65 0 -101.5 -25.5t-36.5 -73.5l-173 14q0 104 87.5 168.5t223.5 64.5q135 0 213 -72t78 -205v-316q0 -97 26 -178h-177q-10 27 -17 68q-77 -82 -201 -82q-118 0 -183.5 61.5t-65.5 166.5zM312 923 q0 -88 117 -88q40 0 82 18.5t69 43.5v136h-106q-76 -1 -119 -31t-43 -79z" />
|
||||
<glyph unicode="«" horiz-adv-x="1023" d="M77 515v19l280 390h186l-240 -400l240 -399h-186zM462 515v19l280 390h186l-240 -400l240 -399h-186z" />
|
||||
<glyph unicode="¬" horiz-adv-x="1129" d="M126 634v171h835v-431h-200v260h-635z" />
|
||||
<glyph unicode="­" horiz-adv-x="794" d="M110 507v233h563v-233h-563z" />
|
||||
<glyph unicode="®" horiz-adv-x="1606" d="M86 729q0 202 93.5 375t259 272.5t357.5 99.5t357.5 -99.5t259 -272.5t93.5 -375q0 -204 -95 -377.5t-259.5 -272.5t-355.5 -99q-193 0 -357.5 100t-258.5 273t-94 376zM208 729q0 -170 77.5 -314t214 -227.5t296.5 -83.5t297.5 85t213.5 229t76 311q0 166 -75 308.5 t-212 228t-300 85.5q-159 0 -295.5 -82t-214.5 -226t-78 -314zM501 316v850h281q151 0 238 -68.5t87 -194.5q0 -112 -113 -174q61 -31 85.5 -86.5t24.5 -137.5t3.5 -116t13.5 -57v-16h-155q-13 34 -13 194q0 76 -33 109.5t-110 33.5h-158v-337h-151zM652 787h136 q74 0 121.5 32t47.5 84q0 70 -35.5 99.5t-128.5 30.5h-141v-246z" />
|
||||
<glyph unicode="¯" horiz-adv-x="1026" d="M148 1290v167h730v-167h-730z" />
|
||||
<glyph unicode="°" horiz-adv-x="795" d="M126 1200q0 114 81 195t191 81q109 0 188.5 -80.5t79.5 -195.5t-79.5 -193.5t-188.5 -78.5q-108 0 -190 78.5t-82 193.5zM273 1200q0 -52 36.5 -88t88.5 -36q53 0 87.5 35.5t34.5 88.5q0 52 -34.5 90t-87.5 38t-89 -38t-36 -90z" />
|
||||
<glyph unicode="±" horiz-adv-x="1100" d="M89 701v241h335v343h253v-343h328v-241h-328v-364h-253v364h-335zM113 1v235h864v-235h-864z" />
|
||||
<glyph unicode="²" horiz-adv-x="763" d="M55 1193q0 116 85.5 195t220.5 79q148 0 228.5 -64.5t80.5 -183.5q0 -70 -36 -128t-144 -145l-148 -115h351v-164h-620v138l287 257q56 49 80.5 91t24.5 65q0 84 -95 84q-50 0 -79.5 -31t-29.5 -78h-206z" />
|
||||
<glyph unicode="³" horiz-adv-x="763" d="M48 902h206q0 -34 34 -58.5t86 -24.5q60 0 86.5 26.5t26.5 61.5q0 92 -122 93h-92v136h82q119 0 119 88q0 35 -28.5 56t-77.5 21q-42 0 -71.5 -15.5t-29.5 -44.5h-205q0 102 84.5 163.5t215.5 61.5q145 0 230.5 -59.5t85.5 -166.5q0 -119 -135 -169q150 -41 150 -184 q0 -105 -91.5 -168.5t-239.5 -63.5q-142 0 -228 66.5t-86 180.5z" />
|
||||
<glyph unicode="´" horiz-adv-x="679" d="M101 1226l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="µ" horiz-adv-x="1261" d="M139 -416v1498h289v-623q0 -126 40.5 -185.5t139.5 -59.5q149 0 205 105v763h289v-1082h-269l-6 68q-89 -89 -225 -89q-102 0 -174 45v-440h-289z" />
|
||||
<glyph unicode="¶" horiz-adv-x="1003" d="M75 988q0 213 133 340.5t363 127.5h298v-1456h-219v520h-80q-230 0 -362.5 127t-132.5 341z" />
|
||||
<glyph unicode="·" horiz-adv-x="617" d="M140 697q0 69 46 112t117 43t117.5 -43t46.5 -112t-47 -111.5t-117 -42.5q-72 0 -117.5 43.5t-45.5 110.5z" />
|
||||
<glyph unicode="¸" horiz-adv-x="548" d="M98 -136l31 143h216l-11 -58q150 -27 150 -173q0 -110 -91.5 -174t-257.5 -64l-7 167q112 0 112 81q0 42 -33.5 57.5t-108.5 20.5z" />
|
||||
<glyph unicode="¹" horiz-adv-x="763" d="M135 1176v158l374 121h19v-786h-204v548z" />
|
||||
<glyph unicode="º" horiz-adv-x="936" d="M118 1049v72q0 160 95.5 257.5t250.5 97.5t251 -97t96 -263v-72q0 -159 -94 -256.5t-251 -97.5q-158 0 -253 98t-95 261zM293 1044q0 -98 46.5 -153t126.5 -55q78 0 123.5 54t46.5 151v80q0 97 -46.5 152t-125.5 55q-78 0 -124.5 -54.5t-46.5 -156.5v-73z" />
|
||||
<glyph unicode="»" horiz-adv-x="1023" d="M85 124l240 399l-240 400h187l280 -390v-19l-280 -390h-187zM478 124l240 399l-240 400h187l280 -390v-19l-280 -390h-187z" />
|
||||
<glyph unicode="¼" horiz-adv-x="1470" d="M101 1171v158l374 121h19v-786h-204v548zM317 193l711 1138l141 -76l-711 -1138zM739 294l357 495h206v-463h88v-167h-88v-159h-205v159h-346zM935 326h162v212l-14 -22z" />
|
||||
<glyph unicode="½" horiz-adv-x="1559" d="M84 1177v158l374 121h19v-786h-204v548zM275 193l711 1138l141 -76l-711 -1138zM839 526q0 116 85.5 195t220.5 79q148 0 228.5 -64.5t80.5 -183.5q0 -70 -36 -128t-144 -145l-148 -115h351v-164h-620v138l287 257q56 49 80.5 91t24.5 65q0 84 -95 84q-50 0 -79.5 -31 t-29.5 -78h-206z" />
|
||||
<glyph unicode="¾" horiz-adv-x="1655" d="M94 903h206q0 -34 34 -58.5t86 -24.5q60 0 86.5 26.5t26.5 61.5q0 92 -122 93h-92v136h82q119 0 119 88q0 35 -28.5 56t-77.5 21q-42 0 -71.5 -15.5t-29.5 -44.5h-205q0 102 84.5 163.5t215.5 61.5q145 0 230.5 -59.5t85.5 -166.5q0 -119 -135 -169q150 -41 150 -184 q0 -105 -91.5 -168.5t-239.5 -63.5q-142 0 -228 66.5t-86 180.5zM478 193l711 1138l141 -76l-711 -1138zM897 294l357 495h206v-463h88v-167h-88v-159h-205v159h-346zM1093 326h162v212l-14 -22z" />
|
||||
<glyph unicode="¿" horiz-adv-x="1019" d="M69 6q0 159 153 312l97 93q50 45 69.5 94t21.5 138h256q0 -133 -31 -215t-110.5 -156t-108 -110t-43 -73t-14.5 -81q0 -157 154 -157q77 0 122.5 45t47.5 126h289q-2 -192 -123 -299.5t-331 -107.5q-213 0 -331 101.5t-118 289.5zM378 949q0 67 44.5 110t117.5 43 t117.5 -43t44.5 -110t-46 -110t-116 -43t-116 43t-46 110z" />
|
||||
<glyph unicode="À" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM323 1846h315l198 -310h-237zM507 543h364l-183 545z" />
|
||||
<glyph unicode="Á" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM507 543h364l-183 545zM553 1536l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="Â" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM312 1554v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226zM507 543h364l-183 545z" />
|
||||
<glyph unicode="Ã" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM315 1566q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5t25.5 70.5l167 -11q0 -113 -66 -189.5t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5 q-35 0 -59.5 -25t-24.5 -71zM507 543h364l-183 545z" />
|
||||
<glyph unicode="Ä" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM309 1675q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM507 543h364l-183 545zM796 1675q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5 q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="Å" horiz-adv-x="1378" d="M7 0l542 1456h278l545 -1456h-319l-101 300h-526l-100 -300h-319zM470 1730q0 86 65 145.5t158 59.5q92 0 157.5 -58.5t65.5 -146.5q0 -85 -64 -143t-159 -58q-97 0 -160 59t-63 142zM507 543h364l-183 545zM585 1730q0 -44 29 -75.5t79 -31.5t79 31.5t29 75.5 q0 46 -29.5 77.5t-78.5 31.5t-78.5 -31.5t-29.5 -77.5z" />
|
||||
<glyph unicode="Æ" horiz-adv-x="1925" d="M2 0l786 1456h1016v-236h-598l15 -355h502v-236h-492l16 -394h618v-235h-897l-14 333h-446l-167 -333h-339zM633 580h311l-24 570z" />
|
||||
<glyph unicode="Ç" horiz-adv-x="1340" d="M86 686v89q0 210 74 370t211.5 245.5t319.5 85.5q252 0 406 -135t178 -379h-300q-11 141 -78.5 204.5t-205.5 63.5q-150 0 -224.5 -107.5t-76.5 -333.5v-110q0 -236 71.5 -345t225.5 -109q139 0 207.5 63.5t78.5 196.5h300q-17 -235 -173.5 -370t-412.5 -135 q-280 0 -440.5 188.5t-160.5 517.5zM550 -137l31 143h216l-11 -58q150 -27 150 -173q0 -110 -91.5 -174t-257.5 -64l-7 167q112 0 112 81q0 42 -33.5 57.5t-108.5 20.5z" />
|
||||
<glyph unicode="È" horiz-adv-x="1152" d="M130 0v1456h974v-243h-674v-347h576v-235h-576v-390h676v-241h-976zM266 1849h315l198 -310h-237z" />
|
||||
<glyph unicode="É" horiz-adv-x="1152" d="M130 0v1456h974v-243h-674v-347h576v-235h-576v-390h676v-241h-976zM496 1539l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="Ê" horiz-adv-x="1152" d="M130 0v1456h974v-243h-674v-347h576v-235h-576v-390h676v-241h-976zM255 1557v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226z" />
|
||||
<glyph unicode="Ë" horiz-adv-x="1152" d="M130 0v1456h974v-243h-674v-347h576v-235h-576v-390h676v-241h-976zM252 1678q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM739 1678q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5 t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="Ì" horiz-adv-x="597" d="M-70 1849h315l198 -310h-237zM149 0v1456h300v-1456h-300z" />
|
||||
<glyph unicode="Í" horiz-adv-x="597" d="M149 0v1456h300v-1456h-300zM159 1539l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="Î" horiz-adv-x="597" d="M-81 1557v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226zM149 0v1456h300v-1456h-300z" />
|
||||
<glyph unicode="Ï" horiz-adv-x="597" d="M-84 1678q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM149 0v1456h300v-1456h-300zM403 1678q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="Ð" horiz-adv-x="1361" d="M-20 642v183h180v631h448q192 0 343.5 -86.5t236.5 -246t85 -362.5v-67q0 -203 -83.5 -361t-235.5 -245t-343 -88h-451v642h-180zM460 241h145q178 0 271 117.5t93 335.5v68q0 222 -92 336.5t-269 114.5h-148v-388h219v-183h-219v-401z" />
|
||||
<glyph unicode="Ñ" horiz-adv-x="1446" d="M130 0v1456h300l585 -960v960h299v-1456h-300l-584 958v-958h-300zM349 1566q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5t25.5 70.5l167 -11q0 -113 -66 -189.5t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5 q-35 0 -59.5 -25t-24.5 -71z" />
|
||||
<glyph unicode="Ò" horiz-adv-x="1414" d="M86 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -215 -76 -377t-217.5 -250t-324.5 -88q-181 0 -323 87t-220 248.5t-79 371.5zM337 1846h315l198 -310h-237zM390 695q0 -223 82 -346t236 -123q151 0 232 118.5t82 345.5v71 q0 229 -82 348t-234 119q-151 0 -233 -117.5t-83 -344.5v-71z" />
|
||||
<glyph unicode="Ó" horiz-adv-x="1414" d="M86 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -215 -76 -377t-217.5 -250t-324.5 -88q-181 0 -323 87t-220 248.5t-79 371.5zM390 695q0 -223 82 -346t236 -123q151 0 232 118.5t82 345.5v71q0 229 -82 348t-234 119 q-151 0 -233 -117.5t-83 -344.5v-71zM567 1536l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="Ô" horiz-adv-x="1414" d="M86 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -215 -76 -377t-217.5 -250t-324.5 -88q-181 0 -323 87t-220 248.5t-79 371.5zM326 1554v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226zM390 695q0 -223 82 -346 t236 -123q151 0 232 118.5t82 345.5v71q0 229 -82 348t-234 119q-151 0 -233 -117.5t-83 -344.5v-71z" />
|
||||
<glyph unicode="Õ" horiz-adv-x="1414" d="M86 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -215 -76 -377t-217.5 -250t-324.5 -88q-181 0 -323 87t-220 248.5t-79 371.5zM329 1566q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5 t25.5 70.5l167 -11q0 -113 -66 -189.5t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5q-35 0 -59.5 -25t-24.5 -71zM390 695q0 -223 82 -346t236 -123q151 0 232 118.5t82 345.5v71q0 229 -82 348t-234 119q-151 0 -233 -117.5t-83 -344.5v-71z" />
|
||||
<glyph unicode="Ö" horiz-adv-x="1414" d="M86 687v72q0 215 77.5 378.5t219 251t323.5 87.5t323.5 -87.5t219 -251t77.5 -377.5v-65q0 -215 -76 -377t-217.5 -250t-324.5 -88q-181 0 -323 87t-220 248.5t-79 371.5zM323 1675q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5 t-39.5 90.5zM390 695q0 -223 82 -346t236 -123q151 0 232 118.5t82 345.5v71q0 229 -82 348t-234 119q-151 0 -233 -117.5t-83 -344.5v-71zM810 1675q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="×" horiz-adv-x="1088" d="M65 373l307 313l-307 313l170 168l304 -311l305 311l170 -168l-307 -313l307 -313l-170 -168l-305 310l-304 -310z" />
|
||||
<glyph unicode="Ø" horiz-adv-x="1411" d="M93 702v57q0 215 77.5 378.5t219 251t323.5 87.5q175 0 314 -82l74 124h187l-134 -227q179 -198 179 -537v-59q0 -215 -76 -377t-217.5 -250t-324.5 -88q-164 0 -295 70l-85 -145h-188l143 242q-197 195 -197 555zM397 695q0 -172 49 -285l447 757q-73 61 -180 61 q-151 0 -233 -117.5t-83 -344.5v-71zM552 273q70 -47 163 -47q151 0 232.5 118.5t82.5 345.5v71q0 151 -38 256z" />
|
||||
<glyph unicode="Ù" horiz-adv-x="1348" d="M116 486v970h300v-961q0 -143 68.5 -208.5t189.5 -65.5q253 0 257 266v969h301v-959q0 -239 -149.5 -378t-408.5 -139q-255 0 -405 135t-153 371zM301 1846h315l198 -310h-237z" />
|
||||
<glyph unicode="Ú" horiz-adv-x="1348" d="M116 486v970h300v-961q0 -143 68.5 -208.5t189.5 -65.5q253 0 257 266v969h301v-959q0 -239 -149.5 -378t-408.5 -139q-255 0 -405 135t-153 371zM531 1536l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="Û" horiz-adv-x="1348" d="M116 486v970h300v-961q0 -143 68.5 -208.5t189.5 -65.5q253 0 257 266v969h301v-959q0 -239 -149.5 -378t-408.5 -139q-255 0 -405 135t-153 371zM290 1554v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226z" />
|
||||
<glyph unicode="Ü" horiz-adv-x="1348" d="M116 486v970h300v-961q0 -143 68.5 -208.5t189.5 -65.5q253 0 257 266v969h301v-959q0 -239 -149.5 -378t-408.5 -139q-255 0 -405 135t-153 371zM287 1675q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM774 1675 q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="Ý" horiz-adv-x="1266" d="M2 1456h329l301 -656l303 656h328l-478 -928v-528h-305v528zM496 1536l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="Þ" horiz-adv-x="1246" d="M133 0v1456h289v-267h230q162 -1 281.5 -56.5t183.5 -158t64 -236.5q0 -202 -138.5 -324t-378.5 -127h-242v-287h-289zM422 520h223q117 0 182 59t65 157t-63.5 158t-175.5 62h-231v-436z" />
|
||||
<glyph unicode="ß" horiz-adv-x="1292" d="M135 0v1101q0 220 124 339t350 119q191 0 306.5 -99.5t115.5 -270.5q0 -108 -53.5 -195t-53.5 -164q0 -37 30.5 -76.5t118.5 -117.5q151 -134 151 -282q0 -177 -115 -275.5t-330 -98.5q-81 0 -160 16t-119 40l54 229q98 -52 219 -52q79 0 121 36.5t42 99.5 q0 46 -34.5 89.5t-116.5 109.5q-150 120 -150 270q0 96 55 186.5t55 169.5q0 70 -44.5 111.5t-112.5 41.5q-159 0 -164 -213v-1114h-289z" />
|
||||
<glyph unicode="à" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M182 1536h315l198 -310h-237zM357 325q0 -54 38 -89t104 -35q64 0 118 28.5t80 76.5v186h-108q-217 0 -231 -150z" />
|
||||
<glyph unicode="á" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M357 325q0 -54 38 -89t104 -35q64 0 118 28.5t80 76.5v186h-108q-217 0 -231 -150zM412 1226l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="â" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M171 1244v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226zM357 325q0 -54 38 -89t104 -35q64 0 118 28.5t80 76.5v186h-108q-217 0 -231 -150z" />
|
||||
<glyph unicode="ã" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M174 1257q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5t25.5 70.5l167 -11q0 -113 -66 -189.5t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5q-35 0 -59.5 -25t-24.5 -71zM357 325q0 -54 38 -89t104 -35q64 0 118 28.5 t80 76.5v186h-108q-217 0 -231 -150z" />
|
||||
<glyph unicode="ä" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M168 1365q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM357 325q0 -54 38 -89t104 -35q64 0 118 28.5t80 76.5v186h-108q-217 0 -231 -150zM655 1365q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5 q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="å" horiz-adv-x="1098" d="M68 304q0 172 127.5 264t368.5 93h133v62q0 75 -38.5 120t-121.5 45q-73 0 -114.5 -35t-41.5 -96h-289q0 94 58 174t164 125.5t238 45.5q200 0 317.5 -100.5t117.5 -282.5v-469q1 -154 43 -233v-17h-292q-20 39 -29 97q-105 -117 -273 -117q-159 0 -263.5 92t-104.5 232z M329 1420q0 86 65 145.5t158 59.5q92 0 157.5 -58.5t65.5 -146.5q0 -85 -64 -143t-159 -58q-97 0 -160 59t-63 142zM357 325q0 -54 38 -89t104 -35q64 0 118 28.5t80 76.5v186h-108q-217 0 -231 -150zM444 1420q0 -44 29 -75.5t79 -31.5t79 31.5t29 75.5q0 46 -29.5 77.5 t-78.5 31.5t-78.5 -31.5t-29.5 -77.5z" />
|
||||
<glyph unicode="æ" horiz-adv-x="1729" d="M66 319q0 157 124 243t367 87h168v57q0 76 -40.5 119t-117.5 43q-82 0 -129.5 -35.5t-47.5 -87.5l-289 19q0 149 130.5 243.5t338.5 94.5q211 0 327 -110q126 112 326 110q212 0 333 -131.5t121 -363.5v-157h-668q11 -116 80.5 -177t186.5 -61q77 0 142.5 16t152.5 61 l77 -189q-73 -56 -180.5 -88t-221.5 -32q-247 0 -386 147q-64 -69 -166.5 -108t-227.5 -39q-186 0 -293 89t-107 250zM355 315q0 -56 40.5 -89.5t125.5 -33.5q49 0 107 22.5t97 57.5v189h-164q-95 -1 -150.5 -43t-55.5 -103zM1011 644h382v28q0 94 -43.5 145t-126.5 51 q-90 0 -144.5 -57.5t-67.5 -166.5z" />
|
||||
<glyph unicode="ç" horiz-adv-x="1068" d="M66 535v19q0 250 133 399t365 149q203 0 325.5 -115.5t124.5 -307.5h-271q-2 84 -52 136.5t-132 52.5q-101 0 -152.5 -73.5t-51.5 -238.5v-30q0 -167 51 -240t155 -73q80 0 130 44t52 117h271q-1 -110 -60 -201.5t-161.5 -142t-226.5 -50.5q-232 0 -366 147.5t-134 407.5 zM419 -137l31 143h216l-11 -58q150 -27 150 -173q0 -110 -91.5 -174t-257.5 -64l-7 167q112 0 112 81q0 42 -33.5 57.5t-108.5 20.5z" />
|
||||
<glyph unicode="è" horiz-adv-x="1107" d="M72 515v28q0 163 63 291.5t178.5 198t263.5 69.5q222 0 349.5 -140t127.5 -397v-118h-689q14 -106 84.5 -170t178.5 -64q167 0 261 121l142 -159q-65 -92 -176 -143.5t-246 -51.5q-238 0 -387.5 146t-149.5 389zM175 1536h315l198 -310h-237zM368 644h402v23 q-2 96 -52 148.5t-142 52.5q-86 0 -139.5 -58t-68.5 -166z" />
|
||||
<glyph unicode="é" horiz-adv-x="1107" d="M72 515v28q0 163 63 291.5t178.5 198t263.5 69.5q222 0 349.5 -140t127.5 -397v-118h-689q14 -106 84.5 -170t178.5 -64q167 0 261 121l142 -159q-65 -92 -176 -143.5t-246 -51.5q-238 0 -387.5 146t-149.5 389zM368 644h402v23q-2 96 -52 148.5t-142 52.5 q-86 0 -139.5 -58t-68.5 -166zM405 1226l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="ê" horiz-adv-x="1107" d="M72 515v28q0 163 63 291.5t178.5 198t263.5 69.5q222 0 349.5 -140t127.5 -397v-118h-689q14 -106 84.5 -170t178.5 -64q167 0 261 121l142 -159q-65 -92 -176 -143.5t-246 -51.5q-238 0 -387.5 146t-149.5 389zM164 1244v16l296 276h168l300 -280v-12h-230l-154 145 l-154 -145h-226zM368 644h402v23q-2 96 -52 148.5t-142 52.5q-86 0 -139.5 -58t-68.5 -166z" />
|
||||
<glyph unicode="ë" horiz-adv-x="1107" d="M72 515v28q0 163 63 291.5t178.5 198t263.5 69.5q222 0 349.5 -140t127.5 -397v-118h-689q14 -106 84.5 -170t178.5 -64q167 0 261 121l142 -159q-65 -92 -176 -143.5t-246 -51.5q-238 0 -387.5 146t-149.5 389zM161 1365q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5 t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM368 644h402v23q-2 96 -52 148.5t-142 52.5q-86 0 -139.5 -58t-68.5 -166zM648 1365q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="ì" horiz-adv-x="561" d="M-88 1521h315l198 -310h-237zM134 0v1082h289v-1082h-289z" />
|
||||
<glyph unicode="í" horiz-adv-x="561" d="M134 0v1082h289v-1082h-289zM141 1211l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="î" horiz-adv-x="561" d="M-99 1229v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226zM134 0v1082h289v-1082h-289z" />
|
||||
<glyph unicode="ï" horiz-adv-x="561" d="M-102 1350q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM134 0v1082h289v-1082h-289zM385 1350q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="ð" horiz-adv-x="1178" d="M84 468q0 231 123 364.5t329 133.5q136 0 244 -76q-49 152 -166 265l-191 -122l-78 114l152 97q-116 72 -264 111l91 224q238 -48 416 -180l171 109l77 -114l-139 -89q255 -262 256 -654v-74q0 -172 -66.5 -309t-185.5 -212.5t-266 -75.5q-144 0 -259 63.5t-179.5 176 t-64.5 248.5zM373 468q0 -112 60 -183.5t158 -71.5q103 0 164 90.5t61 248.5v111q-68 83 -215 83q-113 0 -170.5 -74.5t-57.5 -203.5z" />
|
||||
<glyph unicode="ñ" horiz-adv-x="1147" d="M105 0v1082h272l9 -125q116 145 311 145q172 0 256 -101t86 -302v-699h-289v692q0 92 -40 133.5t-133 41.5q-122 0 -183 -104v-763h-289zM198 1257q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5t25.5 70.5l167 -11q0 -113 -66 -189.5 t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5q-35 0 -59.5 -25t-24.5 -71z" />
|
||||
<glyph unicode="ò" horiz-adv-x="1158" d="M66 538v13q0 161 62 287t178.5 195t270.5 69q219 0 357.5 -134t154.5 -364l2 -74q0 -249 -139 -399.5t-373 -150.5t-373.5 150t-139.5 408zM207 1536h315l198 -310h-237zM355 530q0 -154 58 -235.5t166 -81.5q105 0 164 80.5t59 257.5q0 151 -59 234t-166 83 q-106 0 -164 -82.5t-58 -255.5z" />
|
||||
<glyph unicode="ó" horiz-adv-x="1158" d="M66 538v13q0 161 62 287t178.5 195t270.5 69q219 0 357.5 -134t154.5 -364l2 -74q0 -249 -139 -399.5t-373 -150.5t-373.5 150t-139.5 408zM355 530q0 -154 58 -235.5t166 -81.5q105 0 164 80.5t59 257.5q0 151 -59 234t-166 83q-106 0 -164 -82.5t-58 -255.5zM437 1226 l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="ô" horiz-adv-x="1158" d="M66 538v13q0 161 62 287t178.5 195t270.5 69q219 0 357.5 -134t154.5 -364l2 -74q0 -249 -139 -399.5t-373 -150.5t-373.5 150t-139.5 408zM196 1244v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226zM355 530q0 -154 58 -235.5t166 -81.5q105 0 164 80.5 t59 257.5q0 151 -59 234t-166 83q-106 0 -164 -82.5t-58 -255.5z" />
|
||||
<glyph unicode="õ" horiz-adv-x="1158" d="M66 538v13q0 161 62 287t178.5 195t270.5 69q219 0 357.5 -134t154.5 -364l2 -74q0 -249 -139 -399.5t-373 -150.5t-373.5 150t-139.5 408zM199 1257q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5t25.5 70.5l167 -11 q0 -113 -66 -189.5t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5q-35 0 -59.5 -25t-24.5 -71zM355 530q0 -154 58 -235.5t166 -81.5q105 0 164 80.5t59 257.5q0 151 -59 234t-166 83q-106 0 -164 -82.5t-58 -255.5z" />
|
||||
<glyph unicode="ö" horiz-adv-x="1158" d="M66 538v13q0 161 62 287t178.5 195t270.5 69q219 0 357.5 -134t154.5 -364l2 -74q0 -249 -139 -399.5t-373 -150.5t-373.5 150t-139.5 408zM193 1365q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM355 530 q0 -154 58 -235.5t166 -81.5q105 0 164 80.5t59 257.5q0 151 -59 234t-166 83q-106 0 -164 -82.5t-58 -255.5zM680 1365q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="÷" horiz-adv-x="1168" d="M63 571v230h1028v-230h-1028zM415 277q0 68 45.5 110t117.5 42q71 0 117.5 -41.5t46.5 -110.5q0 -67 -45 -108.5t-119 -41.5q-75 0 -119 42t-44 108zM415 1089q0 68 45.5 110t117.5 42q71 0 117.5 -41.5t46.5 -110.5q0 -67 -45 -108.5t-119 -41.5q-75 0 -119 42t-44 108z " />
|
||||
<glyph unicode="ø" horiz-adv-x="1156" d="M66 551q0 161 62 287t178.5 195t270.5 69q101 0 186 -29l70 143h161l-103 -211q200 -149 200 -475q0 -249 -139 -399.5t-373 -150.5q-95 0 -176 26l-72 -148h-161l103 212q-207 146 -207 481zM355 530q0 -130 41 -208l260 532q-36 14 -79 14q-106 0 -164 -82.5 t-58 -255.5zM509 223q30 -10 70 -10q105 0 164 80.5t59 257.5q0 114 -37 196z" />
|
||||
<glyph unicode="ù" horiz-adv-x="1146" d="M104 373v709h289v-699q0 -169 154 -169q147 0 202 102v766h290v-1082h-272l-8 110q-107 -130 -296 -130q-174 0 -265.5 100t-93.5 293zM203 1536h315l198 -310h-237z" />
|
||||
<glyph unicode="ú" horiz-adv-x="1146" d="M104 373v709h289v-699q0 -169 154 -169q147 0 202 102v766h290v-1082h-272l-8 110q-107 -130 -296 -130q-174 0 -265.5 100t-93.5 293zM433 1226l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="û" horiz-adv-x="1146" d="M104 373v709h289v-699q0 -169 154 -169q147 0 202 102v766h290v-1082h-272l-8 110q-107 -130 -296 -130q-174 0 -265.5 100t-93.5 293zM192 1244v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226z" />
|
||||
<glyph unicode="ü" horiz-adv-x="1146" d="M104 373v709h289v-699q0 -169 154 -169q147 0 202 102v766h290v-1082h-272l-8 110q-107 -130 -296 -130q-174 0 -265.5 100t-93.5 293zM189 1365q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM676 1365 q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="ý" horiz-adv-x="1028" d="M3 1082h311l201 -673l200 673h310l-435 -1250l-24 -57q-97 -212 -320 -212q-63 0 -128 19v219l44 -1q82 0 122.5 25t63.5 83l34 89zM381 1226l197 310h315l-277 -310h-235z" />
|
||||
<glyph unicode="þ" horiz-adv-x="1162" d="M113 -416v1952h290v-547q100 113 262 113q198 0 310 -147t112 -410v-14q0 -250 -113.5 -400.5t-306.5 -150.5q-164 0 -264 113v-509h-290zM403 318q54 -105 188 -105q207 0 207 339q0 151 -53.5 233t-155.5 82q-132 0 -186 -102v-447z" />
|
||||
<glyph unicode="ÿ" horiz-adv-x="1028" d="M3 1082h311l201 -673l200 673h310l-435 -1250l-24 -57q-97 -212 -320 -212q-63 0 -128 19v219l44 -1q82 0 122.5 25t63.5 83l34 89zM137 1365q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM624 1365q0 53 39.5 90 t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="Œ" horiz-adv-x="1983" d="M96 563v317q0 173 75.5 309.5t214 211.5t314.5 75q123 0 290 -20h884v-243h-673v-347h575v-235h-575v-390h675v-241h-886q-167 -20 -288 -20q-174 0 -312 73.5t-214.5 207t-79.5 302.5zM385 576q0 -176 83.5 -270t233.5 -94q94 0 198 13v1004q-112 14 -200 14 q-150 0 -232 -92.5t-83 -265.5v-309z" />
|
||||
<glyph unicode="œ" horiz-adv-x="1847" d="M83 538v13q0 161 61 286.5t177 195t270 69.5q231 0 371 -149q134 151 349 149q214 0 337 -129t123 -365v-158h-654q16 -113 81.5 -175.5t168.5 -62.5q85 0 152 16.5t149 61.5l79 -187q-73 -58 -182 -90.5t-228 -32.5q-233 0 -373 149q-138 -149 -371 -149t-371.5 149.5 t-138.5 408.5zM372 530q0 -155 56.5 -236.5t164.5 -81.5q106 0 163 80.5t57 258.5q0 153 -58 235t-164 82q-105 0 -162 -82t-57 -256zM1120 647h367v26q0 97 -46.5 146t-129.5 49q-77 0 -126 -57t-65 -164z" />
|
||||
<glyph unicode="Ÿ" horiz-adv-x="1266" d="M2 1456h329l301 -656l303 656h328l-478 -928v-528h-305v528zM252 1675q0 54 38.5 90.5t99.5 36.5q62 0 100 -37.5t38 -89.5t-38 -89t-100 -37q-59 0 -98.5 35.5t-39.5 90.5zM739 1675q0 53 39.5 90t98.5 37q58 0 98.5 -36.5t40.5 -90.5q0 -52 -38.5 -89.5t-100.5 -37.5 t-100 37.5t-38 89.5z" />
|
||||
<glyph unicode="ˆ" horiz-adv-x="1015" d="M123 1244v16l296 276h168l300 -280v-12h-230l-154 145l-154 -145h-226z" />
|
||||
<glyph unicode="˜" horiz-adv-x="985" d="M117 1258q0 111 65.5 189t160.5 78q30 0 56.5 -7.5t86.5 -36.5t83 -35t48 -6q35 0 60.5 24.5t25.5 70.5l167 -11q0 -113 -66 -189.5t-161 -76.5q-38 0 -67.5 8.5t-81.5 36.5t-75 34.5t-50 6.5q-35 0 -59.5 -25t-24.5 -71z" />
|
||||
<glyph unicode=" " horiz-adv-x="967" />
|
||||
<glyph unicode=" " horiz-adv-x="1935" />
|
||||
<glyph unicode=" " horiz-adv-x="967" />
|
||||
<glyph unicode=" " horiz-adv-x="1935" />
|
||||
<glyph unicode=" " horiz-adv-x="645" />
|
||||
<glyph unicode=" " horiz-adv-x="483" />
|
||||
<glyph unicode=" " horiz-adv-x="322" />
|
||||
<glyph unicode=" " horiz-adv-x="322" />
|
||||
<glyph unicode=" " horiz-adv-x="241" />
|
||||
<glyph unicode=" " horiz-adv-x="387" />
|
||||
<glyph unicode=" " horiz-adv-x="107" />
|
||||
<glyph unicode="‐" horiz-adv-x="794" d="M110 507v233h563v-233h-563z" />
|
||||
<glyph unicode="‑" horiz-adv-x="794" d="M110 507v233h563v-233h-563z" />
|
||||
<glyph unicode="‒" horiz-adv-x="794" d="M110 507v233h563v-233h-563z" />
|
||||
<glyph unicode="–" horiz-adv-x="1294" d="M147 596v236h1036v-236h-1036z" />
|
||||
<glyph unicode="—" horiz-adv-x="1563" d="M33 596v236h1381v-236h-1381z" />
|
||||
<glyph unicode="‘" horiz-adv-x="479" d="M104 1048v150q0 94 52.5 200.5t129.5 171.5l136 -79q-86 -136 -89 -276v-167h-229z" />
|
||||
<glyph unicode="’" horiz-adv-x="470" d="M58 1088q86 135 89 279v169h230v-155q0 -90 -50 -195t-133 -177z" />
|
||||
<glyph unicode="‚" horiz-adv-x="508" d="M66 -226q78 126 81 274v181h238l-1 -166q-1 -89 -50.5 -192t-131.5 -176z" />
|
||||
<glyph unicode="“" horiz-adv-x="831" d="M112 1048v150q0 94 52.5 200.5t129.5 171.5l136 -79q-86 -136 -89 -276v-167h-229zM455 1048v150q0 94 52.5 200.5t129.5 171.5l136 -79q-86 -136 -89 -276v-167h-229z" />
|
||||
<glyph unicode="”" horiz-adv-x="837" d="M72 1088q86 135 89 279v169h230v-155q0 -90 -50 -195t-133 -177zM419 1088q86 135 89 279v169h230v-155q0 -90 -50 -195t-133 -177z" />
|
||||
<glyph unicode="„" horiz-adv-x="825" d="M66 -246q78 134 81 293v216h238l-1 -199q-1 -97 -48 -206.5t-127 -182.5zM402 -246q86 148 89 294v215h238l-1 -203q-2 -96 -52.5 -205t-130.5 -180z" />
|
||||
<glyph unicode="•" horiz-adv-x="736" d="M135 731v35q0 104 66 167t170 63q108 0 172.5 -62t66.5 -163v-43q0 -103 -65.5 -165.5t-171.5 -62.5q-105 0 -171.5 62t-66.5 169z" />
|
||||
<glyph unicode="…" horiz-adv-x="1515" d="M133 142q0 69 46.5 112t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5zM606 142q0 69 46.5 112t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5zM1070 142q0 69 46.5 112 t116.5 43q71 0 117.5 -43t46.5 -112q0 -68 -46 -110.5t-118 -42.5q-71 0 -117 42.5t-46 110.5z" />
|
||||
<glyph unicode=" " horiz-adv-x="387" />
|
||||
<glyph unicode="‹" horiz-adv-x="638" d="M108 515v19l280 390h186l-240 -400l240 -399h-186z" />
|
||||
<glyph unicode="›" horiz-adv-x="618" d="M80 124l240 399l-240 400h187l280 -390v-19l-280 -390h-187z" />
|
||||
<glyph unicode=" " horiz-adv-x="483" />
|
||||
<glyph unicode="€" d="M89 516v152h169v124h-169v152h171q17 252 175.5 391.5t420.5 139.5q105 0 236 -31l-36 -243q-94 32 -185 32q-283 0 -310 -289h333v-152h-335v-124h335v-152h-335q5 -147 78.5 -221t228.5 -74q105 0 190 31l36 -242q-124 -29 -256 -29q-263 0 -416.5 141.5t-161.5 393.5 h-169z" />
|
||||
<glyph unicode="™" horiz-adv-x="1293" d="M116 1348v108h407v-108h-129v-431h-142v431h-136zM594 914v542h158l117 -368l126 368h149v-542h-128v352l-111 -352h-72l-111 353v-353h-128z" />
|
||||
<glyph unicode="◼" horiz-adv-x="1080" d="M0 0v1080h1080v-1080h-1080z" />
|
||||
<glyph unicode="ffi" horiz-adv-x="1901" d="M29 870v212h161v92q0 182 104.5 282.5t292.5 100.5q60 0 147 -20l-3 -224q-36 9 -88 9q-163 0 -163 -153v-87h356v50q2 204 125.5 314.5t348.5 110.5q134 0 329 -59l-42 -239q-98 29 -152.5 37.5t-116.5 8.5q-202 0 -202 -179v-44h213v-212h-213v-870h-290v870h-356v-870 h-290v870h-161zM1483 0v1082h290v-1082h-290z" />
|
||||
<glyph unicode="ffl" horiz-adv-x="1901" d="M29 870v212h161v92q0 182 104.5 282.5t292.5 100.5q60 0 147 -20l-3 -224q-36 9 -88 9q-163 0 -163 -153v-87h356v81q1 191 118.5 292.5t328.5 101.5q144 0 490 -32v-1525h-290v1312q-86 10 -161 10q-196 0 -196 -167v-73h215v-212h-215v-870h-290v870h-356v-870h-290 v870h-161z" />
|
||||
<hkern u1=" " u2="T" k="60" />
|
||||
<hkern u1=""" u2="w" k="-11" />
|
||||
<hkern u1="'" u2="w" k="-11" />
|
||||
<hkern u1="(" u2="Ÿ" k="-22" />
|
||||
<hkern u1="(" u2="Ý" k="-22" />
|
||||
<hkern u1="(" u2="Y" k="-22" />
|
||||
<hkern u1="(" u2="W" k="-38" />
|
||||
<hkern u1="(" u2="V" k="-20" />
|
||||
<hkern u1="/" u2="/" k="248" />
|
||||
<hkern u1="A" u2="w" k="33" />
|
||||
<hkern u1="A" u2="t" k="17" />
|
||||
<hkern u1="A" u2="?" k="81" />
|
||||
<hkern u1="C" u2="}" k="17" />
|
||||
<hkern u1="C" u2="]" k="12" />
|
||||
<hkern u1="C" u2=")" k="26" />
|
||||
<hkern u1="D" u2="Æ" k="33" />
|
||||
<hkern u1="E" u2="w" k="22" />
|
||||
<hkern u1="E" u2="f" k="18" />
|
||||
<hkern u1="F" u2="…" k="274" />
|
||||
<hkern u1="F" u2="„" k="274" />
|
||||
<hkern u1="F" u2="‚" k="274" />
|
||||
<hkern u1="F" u2="œ" k="21" />
|
||||
<hkern u1="F" u2="ÿ" k="24" />
|
||||
<hkern u1="F" u2="ý" k="24" />
|
||||
<hkern u1="F" u2="ü" k="22" />
|
||||
<hkern u1="F" u2="û" k="22" />
|
||||
<hkern u1="F" u2="ú" k="22" />
|
||||
<hkern u1="F" u2="ù" k="22" />
|
||||
<hkern u1="F" u2="ö" k="21" />
|
||||
<hkern u1="F" u2="õ" k="21" />
|
||||
<hkern u1="F" u2="ô" k="21" />
|
||||
<hkern u1="F" u2="ó" k="21" />
|
||||
<hkern u1="F" u2="ò" k="21" />
|
||||
<hkern u1="F" u2="ë" k="21" />
|
||||
<hkern u1="F" u2="ê" k="21" />
|
||||
<hkern u1="F" u2="é" k="21" />
|
||||
<hkern u1="F" u2="è" k="21" />
|
||||
<hkern u1="F" u2="ç" k="21" />
|
||||
<hkern u1="F" u2="å" k="34" />
|
||||
<hkern u1="F" u2="ä" k="34" />
|
||||
<hkern u1="F" u2="ã" k="34" />
|
||||
<hkern u1="F" u2="â" k="34" />
|
||||
<hkern u1="F" u2="á" k="34" />
|
||||
<hkern u1="F" u2="à" k="34" />
|
||||
<hkern u1="F" u2="Å" k="192" />
|
||||
<hkern u1="F" u2="Ä" k="192" />
|
||||
<hkern u1="F" u2="Ã" k="192" />
|
||||
<hkern u1="F" u2="Â" k="192" />
|
||||
<hkern u1="F" u2="Á" k="192" />
|
||||
<hkern u1="F" u2="À" k="192" />
|
||||
<hkern u1="F" u2="y" k="24" />
|
||||
<hkern u1="F" u2="v" k="24" />
|
||||
<hkern u1="F" u2="u" k="22" />
|
||||
<hkern u1="F" u2="r" k="26" />
|
||||
<hkern u1="F" u2="q" k="21" />
|
||||
<hkern u1="F" u2="o" k="21" />
|
||||
<hkern u1="F" u2="g" k="21" />
|
||||
<hkern u1="F" u2="e" k="21" />
|
||||
<hkern u1="F" u2="d" k="21" />
|
||||
<hkern u1="F" u2="c" k="21" />
|
||||
<hkern u1="F" u2="a" k="34" />
|
||||
<hkern u1="F" u2="T" k="-20" />
|
||||
<hkern u1="F" u2="J" k="208" />
|
||||
<hkern u1="F" u2="A" k="192" />
|
||||
<hkern u1="F" u2="." k="274" />
|
||||
<hkern u1="F" u2="," k="274" />
|
||||
<hkern u1="K" u2="w" k="63" />
|
||||
<hkern u1="L" u2="w" k="52" />
|
||||
<hkern u1="O" u2="Æ" k="33" />
|
||||
<hkern u1="P" u2="Æ" k="297" />
|
||||
<hkern u1="P" u2="t" k="-14" />
|
||||
<hkern u1="Q" u2="Ÿ" k="35" />
|
||||
<hkern u1="Q" u2="Ý" k="35" />
|
||||
<hkern u1="Q" u2="Y" k="35" />
|
||||
<hkern u1="Q" u2="W" k="20" />
|
||||
<hkern u1="Q" u2="V" k="28" />
|
||||
<hkern u1="Q" u2="T" k="33" />
|
||||
<hkern u1="R" u2="Ÿ" k="48" />
|
||||
<hkern u1="R" u2="Ý" k="48" />
|
||||
<hkern u1="R" u2="Y" k="48" />
|
||||
<hkern u1="R" u2="V" k="19" />
|
||||
<hkern u1="R" u2="T" k="50" />
|
||||
<hkern u1="T" u2="ø" k="95" />
|
||||
<hkern u1="T" u2="æ" k="84" />
|
||||
<hkern u1="T" u2="Æ" k="189" />
|
||||
<hkern u1="T" u2="»" k="146" />
|
||||
<hkern u1="T" u2="«" k="148" />
|
||||
<hkern u1="T" u2="w" k="47" />
|
||||
<hkern u1="T" u2="r" k="65" />
|
||||
<hkern u1="T" u2=" " k="60" />
|
||||
<hkern u1="V" u2="}" k="-19" />
|
||||
<hkern u1="V" u2="r" k="30" />
|
||||
<hkern u1="V" u2="]" k="-17" />
|
||||
<hkern u1="V" u2=")" k="-20" />
|
||||
<hkern u1="W" u2="}" k="-14" />
|
||||
<hkern u1="W" u2="r" k="21" />
|
||||
<hkern u1="W" u2="]" k="-12" />
|
||||
<hkern u1="W" u2=")" k="-15" />
|
||||
<hkern u1="Y" u2="•" k="45" />
|
||||
<hkern u1="Y" u2="ø" k="64" />
|
||||
<hkern u1="Y" u2="æ" k="63" />
|
||||
<hkern u1="Y" u2="Æ" k="96" />
|
||||
<hkern u1="Y" u2="»" k="51" />
|
||||
<hkern u1="Y" u2="«" k="82" />
|
||||
<hkern u1="Y" u2="}" k="-19" />
|
||||
<hkern u1="Y" u2="t" k="22" />
|
||||
<hkern u1="Y" u2="r" k="40" />
|
||||
<hkern u1="Y" u2="f" k="22" />
|
||||
<hkern u1="Y" u2="]" k="-18" />
|
||||
<hkern u1="Y" u2="*" k="49" />
|
||||
<hkern u1="Y" u2=")" k="-20" />
|
||||
<hkern u1="Y" u2="&" k="30" />
|
||||
<hkern u1="Z" u2="w" k="27" />
|
||||
<hkern u1="[" u2="Ü" k="18" />
|
||||
<hkern u1="[" u2="Û" k="18" />
|
||||
<hkern u1="[" u2="Ú" k="18" />
|
||||
<hkern u1="[" u2="Ù" k="18" />
|
||||
<hkern u1="[" u2="U" k="18" />
|
||||
<hkern u1="[" u2="J" k="18" />
|
||||
<hkern u1="e" u2="’" k="64" />
|
||||
<hkern u1="f" u2="”" k="-16" />
|
||||
<hkern u1="f" u2="“" k="-16" />
|
||||
<hkern u1="f" u2="’" k="-16" />
|
||||
<hkern u1="f" u2="‘" k="-16" />
|
||||
<hkern u1="f" u2="œ" k="24" />
|
||||
<hkern u1="f" u2="ë" k="24" />
|
||||
<hkern u1="f" u2="ê" k="24" />
|
||||
<hkern u1="f" u2="é" k="24" />
|
||||
<hkern u1="f" u2="è" k="24" />
|
||||
<hkern u1="f" u2="ç" k="24" />
|
||||
<hkern u1="f" u2="}" k="-19" />
|
||||
<hkern u1="f" u2="q" k="24" />
|
||||
<hkern u1="f" u2="g" k="24" />
|
||||
<hkern u1="f" u2="e" k="24" />
|
||||
<hkern u1="f" u2="d" k="24" />
|
||||
<hkern u1="f" u2="c" k="24" />
|
||||
<hkern u1="f" u2="]" k="-18" />
|
||||
<hkern u1="f" u2=")" k="-20" />
|
||||
<hkern u1="f" u2="'" k="-16" />
|
||||
<hkern u1="f" u2=""" k="-16" />
|
||||
<hkern u1="h" u2="’" k="104" />
|
||||
<hkern u1="k" u2="œ" k="20" />
|
||||
<hkern u1="k" u2="ë" k="20" />
|
||||
<hkern u1="k" u2="ê" k="20" />
|
||||
<hkern u1="k" u2="é" k="20" />
|
||||
<hkern u1="k" u2="è" k="20" />
|
||||
<hkern u1="k" u2="ç" k="20" />
|
||||
<hkern u1="k" u2="q" k="20" />
|
||||
<hkern u1="k" u2="g" k="20" />
|
||||
<hkern u1="k" u2="e" k="20" />
|
||||
<hkern u1="k" u2="d" k="20" />
|
||||
<hkern u1="k" u2="c" k="20" />
|
||||
<hkern u1="m" u2="’" k="120" />
|
||||
<hkern u1="n" u2="’" k="120" />
|
||||
<hkern u1="o" u2="’" k="112" />
|
||||
<hkern u1="r" u2="’" k="-16" />
|
||||
<hkern u1="r" u2="w" k="-17" />
|
||||
<hkern u1="r" u2="t" k="-50" />
|
||||
<hkern u1="r" u2="f" k="-20" />
|
||||
<hkern u1="t" u2="’" k="-24" />
|
||||
<hkern u1="t" u2="ö" k="30" />
|
||||
<hkern u1="t" u2="õ" k="30" />
|
||||
<hkern u1="t" u2="ô" k="30" />
|
||||
<hkern u1="t" u2="ó" k="30" />
|
||||
<hkern u1="t" u2="ò" k="30" />
|
||||
<hkern u1="t" u2="o" k="30" />
|
||||
<hkern u1="v" u2="f" k="-13" />
|
||||
<hkern u1="w" u2="…" k="124" />
|
||||
<hkern u1="w" u2="„" k="124" />
|
||||
<hkern u1="w" u2="‚" k="124" />
|
||||
<hkern u1="w" u2="." k="124" />
|
||||
<hkern u1="w" u2="," k="124" />
|
||||
<hkern u1="y" u2="f" k="-13" />
|
||||
<hkern u1="{" u2="Ü" k="20" />
|
||||
<hkern u1="{" u2="Û" k="20" />
|
||||
<hkern u1="{" u2="Ú" k="20" />
|
||||
<hkern u1="{" u2="Ù" k="20" />
|
||||
<hkern u1="{" u2="U" k="20" />
|
||||
<hkern u1="{" u2="J" k="20" />
|
||||
<hkern u1="À" u2="w" k="33" />
|
||||
<hkern u1="À" u2="t" k="17" />
|
||||
<hkern u1="À" u2="?" k="81" />
|
||||
<hkern u1="Á" u2="w" k="33" />
|
||||
<hkern u1="Á" u2="t" k="17" />
|
||||
<hkern u1="Á" u2="?" k="81" />
|
||||
<hkern u1="Â" u2="w" k="33" />
|
||||
<hkern u1="Â" u2="t" k="17" />
|
||||
<hkern u1="Â" u2="?" k="81" />
|
||||
<hkern u1="Ã" u2="w" k="33" />
|
||||
<hkern u1="Ã" u2="t" k="17" />
|
||||
<hkern u1="Ã" u2="?" k="81" />
|
||||
<hkern u1="Ä" u2="w" k="33" />
|
||||
<hkern u1="Ä" u2="t" k="17" />
|
||||
<hkern u1="Ä" u2="?" k="81" />
|
||||
<hkern u1="Å" u2="w" k="33" />
|
||||
<hkern u1="Å" u2="t" k="17" />
|
||||
<hkern u1="Å" u2="?" k="81" />
|
||||
<hkern u1="Ç" u2="}" k="17" />
|
||||
<hkern u1="Ç" u2="]" k="12" />
|
||||
<hkern u1="Ç" u2=")" k="26" />
|
||||
<hkern u1="È" u2="w" k="22" />
|
||||
<hkern u1="È" u2="f" k="18" />
|
||||
<hkern u1="É" u2="w" k="22" />
|
||||
<hkern u1="É" u2="f" k="18" />
|
||||
<hkern u1="Ê" u2="w" k="22" />
|
||||
<hkern u1="Ê" u2="f" k="18" />
|
||||
<hkern u1="Ë" u2="w" k="22" />
|
||||
<hkern u1="Ë" u2="f" k="18" />
|
||||
<hkern u1="Ð" u2="Æ" k="33" />
|
||||
<hkern u1="Ò" u2="Æ" k="33" />
|
||||
<hkern u1="Ó" u2="Æ" k="33" />
|
||||
<hkern u1="Ô" u2="Æ" k="33" />
|
||||
<hkern u1="Õ" u2="Æ" k="33" />
|
||||
<hkern u1="Ö" u2="Æ" k="33" />
|
||||
<hkern u1="Ý" u2="•" k="45" />
|
||||
<hkern u1="Ý" u2="ø" k="64" />
|
||||
<hkern u1="Ý" u2="æ" k="63" />
|
||||
<hkern u1="Ý" u2="Æ" k="96" />
|
||||
<hkern u1="Ý" u2="»" k="51" />
|
||||
<hkern u1="Ý" u2="«" k="82" />
|
||||
<hkern u1="Ý" u2="}" k="-19" />
|
||||
<hkern u1="Ý" u2="t" k="22" />
|
||||
<hkern u1="Ý" u2="r" k="40" />
|
||||
<hkern u1="Ý" u2="f" k="22" />
|
||||
<hkern u1="Ý" u2="]" k="-18" />
|
||||
<hkern u1="Ý" u2="*" k="49" />
|
||||
<hkern u1="Ý" u2=")" k="-20" />
|
||||
<hkern u1="Ý" u2="&" k="30" />
|
||||
<hkern u1="è" u2="’" k="64" />
|
||||
<hkern u1="é" u2="’" k="64" />
|
||||
<hkern u1="ê" u2="’" k="64" />
|
||||
<hkern u1="ë" u2="’" k="64" />
|
||||
<hkern u1="ñ" u2="’" k="120" />
|
||||
<hkern u1="ò" u2="’" k="112" />
|
||||
<hkern u1="ó" u2="’" k="112" />
|
||||
<hkern u1="ô" u2="’" k="112" />
|
||||
<hkern u1="õ" u2="’" k="112" />
|
||||
<hkern u1="ö" u2="’" k="112" />
|
||||
<hkern u1="ý" u2="f" k="-13" />
|
||||
<hkern u1="ÿ" u2="f" k="-13" />
|
||||
<hkern u1="Ÿ" u2="•" k="45" />
|
||||
<hkern u1="Ÿ" u2="ø" k="64" />
|
||||
<hkern u1="Ÿ" u2="æ" k="63" />
|
||||
<hkern u1="Ÿ" u2="Æ" k="96" />
|
||||
<hkern u1="Ÿ" u2="»" k="51" />
|
||||
<hkern u1="Ÿ" u2="«" k="82" />
|
||||
<hkern u1="Ÿ" u2="}" k="-19" />
|
||||
<hkern u1="Ÿ" u2="t" k="22" />
|
||||
<hkern u1="Ÿ" u2="r" k="40" />
|
||||
<hkern u1="Ÿ" u2="f" k="22" />
|
||||
<hkern u1="Ÿ" u2="]" k="-18" />
|
||||
<hkern u1="Ÿ" u2="*" k="49" />
|
||||
<hkern u1="Ÿ" u2=")" k="-20" />
|
||||
<hkern u1="Ÿ" u2="&" k="30" />
|
||||
<hkern u1="‘" u2="w" k="-11" />
|
||||
<hkern u1="’" u2="œ" k="104" />
|
||||
<hkern u1="’" u2="ö" k="144" />
|
||||
<hkern u1="’" u2="õ" k="144" />
|
||||
<hkern u1="’" u2="ô" k="144" />
|
||||
<hkern u1="’" u2="ó" k="144" />
|
||||
<hkern u1="’" u2="ò" k="144" />
|
||||
<hkern u1="’" u2="ë" k="104" />
|
||||
<hkern u1="’" u2="ê" k="104" />
|
||||
<hkern u1="’" u2="é" k="104" />
|
||||
<hkern u1="’" u2="è" k="104" />
|
||||
<hkern u1="’" u2="ç" k="104" />
|
||||
<hkern u1="’" u2="w" k="-11" />
|
||||
<hkern u1="’" u2="s" k="232" />
|
||||
<hkern u1="’" u2="q" k="104" />
|
||||
<hkern u1="’" u2="o" k="144" />
|
||||
<hkern u1="’" u2="g" k="104" />
|
||||
<hkern u1="’" u2="e" k="104" />
|
||||
<hkern u1="’" u2="d" k="104" />
|
||||
<hkern u1="’" u2="c" k="104" />
|
||||
<hkern u1="“" u2="w" k="-11" />
|
||||
<hkern u1="”" u2="w" k="-11" />
|
||||
<hkern g1="B" g2="V" k="24" />
|
||||
<hkern g1="B" g2="Y,Yacute,Ydieresis" k="55" />
|
||||
<hkern g1="B" g2="T" k="27" />
|
||||
<hkern g1="H,I,M,N,Igrave,Iacute,Icircumflex,Idieresis,Ntilde" g2="Y,Yacute,Ydieresis" k="28" />
|
||||
<hkern g1="H,I,M,N,Igrave,Iacute,Icircumflex,Idieresis,Ntilde" g2="T" k="29" />
|
||||
<hkern g1="H,I,M,N,Igrave,Iacute,Icircumflex,Idieresis,Ntilde" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="-18" />
|
||||
<hkern g1="H,I,M,N,Igrave,Iacute,Icircumflex,Idieresis,Ntilde" g2="X" k="-17" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="V" k="22" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="Y,Yacute,Ydieresis" k="43" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="T" k="85" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="21" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="X" k="22" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="Z" k="23" />
|
||||
<hkern g1="D,O,Eth,Ograve,Oacute,Ocircumflex,Otilde,Odieresis" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="122" />
|
||||
<hkern g1="C,Ccedilla" g2="T" k="29" />
|
||||
<hkern g1="E,Egrave,Eacute,Ecircumflex,Edieresis" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="19" />
|
||||
<hkern g1="E,Egrave,Eacute,Ecircumflex,Edieresis" g2="v,y,yacute,ydieresis" k="26" />
|
||||
<hkern g1="E,Egrave,Eacute,Ecircumflex,Edieresis" g2="T" k="-20" />
|
||||
<hkern g1="E,Egrave,Eacute,Ecircumflex,Edieresis" g2="u,ugrave,uacute,ucircumflex,udieresis" k="17" />
|
||||
<hkern g1="E,Egrave,Eacute,Ecircumflex,Edieresis" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="19" />
|
||||
<hkern g1="T" g2="z" k="60" />
|
||||
<hkern g1="T" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="208" />
|
||||
<hkern g1="T" g2="v,y,yacute,ydieresis" k="82" />
|
||||
<hkern g1="T" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="28" />
|
||||
<hkern g1="T" g2="V" k="-16" />
|
||||
<hkern g1="T" g2="m,n,p,ntilde" k="89" />
|
||||
<hkern g1="T" g2="Y,Yacute,Ydieresis" k="-16" />
|
||||
<hkern g1="T" g2="T" k="-16" />
|
||||
<hkern g1="T" g2="u,ugrave,uacute,ucircumflex,udieresis" k="65" />
|
||||
<hkern g1="T" g2="W" k="-15" />
|
||||
<hkern g1="T" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="120" />
|
||||
<hkern g1="T" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="258" />
|
||||
<hkern g1="T" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="89" />
|
||||
<hkern g1="T" g2="x" k="77" />
|
||||
<hkern g1="T" g2="s" k="76" />
|
||||
<hkern g1="T" g2="hyphen,uni00AD,endash,emdash" k="272" />
|
||||
<hkern g1="T" g2="S" k="16" />
|
||||
<hkern g1="T" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="168" />
|
||||
<hkern g1="T" g2="J" k="216" />
|
||||
<hkern g1="K" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="27" />
|
||||
<hkern g1="K" g2="v,y,yacute,ydieresis" k="40" />
|
||||
<hkern g1="K" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="31" />
|
||||
<hkern g1="K" g2="u,ugrave,uacute,ucircumflex,udieresis" k="23" />
|
||||
<hkern g1="K" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="26" />
|
||||
<hkern g1="K" g2="hyphen,uni00AD,endash,emdash" k="164" />
|
||||
<hkern g1="L" g2="v,y,yacute,ydieresis" k="123" />
|
||||
<hkern g1="L" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="64" />
|
||||
<hkern g1="L" g2="V" k="206" />
|
||||
<hkern g1="L" g2="U,Ugrave,Uacute,Ucircumflex,Udieresis" k="24" />
|
||||
<hkern g1="L" g2="Y,Yacute,Ydieresis" k="279" />
|
||||
<hkern g1="L" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="288" />
|
||||
<hkern g1="L" g2="T" k="205" />
|
||||
<hkern g1="L" g2="u,ugrave,uacute,ucircumflex,udieresis" k="14" />
|
||||
<hkern g1="L" g2="W" k="93" />
|
||||
<hkern g1="L" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="-19" />
|
||||
<hkern g1="P" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="13" />
|
||||
<hkern g1="P" g2="v,y,yacute,ydieresis" k="-15" />
|
||||
<hkern g1="P" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="178" />
|
||||
<hkern g1="P" g2="X" k="51" />
|
||||
<hkern g1="P" g2="Z" k="36" />
|
||||
<hkern g1="P" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="404" />
|
||||
<hkern g1="P" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="13" />
|
||||
<hkern g1="P" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="11" />
|
||||
<hkern g1="P" g2="J" k="184" />
|
||||
<hkern g1="J,U,Ugrave,Uacute,Ucircumflex,Udieresis" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="22" />
|
||||
<hkern g1="V" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="46" />
|
||||
<hkern g1="V" g2="v,y,yacute,ydieresis" k="11" />
|
||||
<hkern g1="V" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="13" />
|
||||
<hkern g1="V" g2="u,ugrave,uacute,ucircumflex,udieresis" k="28" />
|
||||
<hkern g1="V" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="75" />
|
||||
<hkern g1="V" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="215" />
|
||||
<hkern g1="V" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="44" />
|
||||
<hkern g1="V" g2="hyphen,uni00AD,endash,emdash" k="157" />
|
||||
<hkern g1="V" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="46" />
|
||||
<hkern g1="X" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="21" />
|
||||
<hkern g1="X" g2="v,y,yacute,ydieresis" k="31" />
|
||||
<hkern g1="X" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="25" />
|
||||
<hkern g1="X" g2="V" k="-14" />
|
||||
<hkern g1="X" g2="u,ugrave,uacute,ucircumflex,udieresis" k="21" />
|
||||
<hkern g1="X" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="26" />
|
||||
<hkern g1="X" g2="hyphen,uni00AD,endash,emdash" k="156" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="z" k="30" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="65" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="v,y,yacute,ydieresis" k="20" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="29" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="V" k="-18" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="U,Ugrave,Uacute,Ucircumflex,Udieresis" k="96" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="m,n,p,ntilde" k="40" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="Y,Yacute,Ydieresis" k="-18" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="T" k="-17" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="u,ugrave,uacute,ucircumflex,udieresis" k="39" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="W" k="-17" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="150" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="X" k="-13" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="231" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="65" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="x" k="23" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="s" k="58" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="hyphen,uni00AD,endash,emdash" k="152" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="S" k="16" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="63" />
|
||||
<hkern g1="Y,Yacute,Ydieresis" g2="J" k="96" />
|
||||
<hkern g1="W" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="31" />
|
||||
<hkern g1="W" g2="T" k="-14" />
|
||||
<hkern g1="W" g2="u,ugrave,uacute,ucircumflex,udieresis" k="19" />
|
||||
<hkern g1="W" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="43" />
|
||||
<hkern g1="W" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="143" />
|
||||
<hkern g1="W" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="31" />
|
||||
<hkern g1="W" g2="hyphen,uni00AD,endash,emdash" k="60" />
|
||||
<hkern g1="W" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="33" />
|
||||
<hkern g1="Z" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="21" />
|
||||
<hkern g1="Z" g2="v,y,yacute,ydieresis" k="27" />
|
||||
<hkern g1="Z" g2="C,G,O,Q,Ccedilla,Ograve,Oacute,Ocircumflex,Otilde,Odieresis,Oslash,OE" k="26" />
|
||||
<hkern g1="Z" g2="u,ugrave,uacute,ucircumflex,udieresis" k="19" />
|
||||
<hkern g1="Z" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="-13" />
|
||||
<hkern g1="Z" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="21" />
|
||||
<hkern g1="a,agrave,aacute,acircumflex,atilde,adieresis,aring" g2="v,y,yacute,ydieresis" k="15" />
|
||||
<hkern g1="a,agrave,aacute,acircumflex,atilde,adieresis,aring" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="17" />
|
||||
<hkern g1="c,ccedilla" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="11" />
|
||||
<hkern g1="b,p,thorn" g2="z" k="15" />
|
||||
<hkern g1="b,p,thorn" g2="v,y,yacute,ydieresis" k="11" />
|
||||
<hkern g1="b,p,thorn" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="29" />
|
||||
<hkern g1="b,p,thorn" g2="x" k="15" />
|
||||
<hkern g1="e,egrave,eacute,ecircumflex,edieresis" g2="v,y,yacute,ydieresis" k="13" />
|
||||
<hkern g1="e,egrave,eacute,ecircumflex,edieresis" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="14" />
|
||||
<hkern g1="h,m,n,ntilde" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="80" />
|
||||
<hkern g1="o,ograve,oacute,ocircumflex,otilde,odieresis" g2="z" k="16" />
|
||||
<hkern g1="o,ograve,oacute,ocircumflex,otilde,odieresis" g2="v,y,yacute,ydieresis" k="15" />
|
||||
<hkern g1="o,ograve,oacute,ocircumflex,otilde,odieresis" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="88" />
|
||||
<hkern g1="o,ograve,oacute,ocircumflex,otilde,odieresis" g2="x" k="21" />
|
||||
<hkern g1="v,y,yacute,ydieresis" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="15" />
|
||||
<hkern g1="v,y,yacute,ydieresis" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="-15" />
|
||||
<hkern g1="v,y,yacute,ydieresis" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="167" />
|
||||
<hkern g1="v,y,yacute,ydieresis" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="13" />
|
||||
<hkern g1="v,y,yacute,ydieresis" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="15" />
|
||||
<hkern g1="r" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="36" />
|
||||
<hkern g1="r" g2="v,y,yacute,ydieresis" k="-18" />
|
||||
<hkern g1="r" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="-16" />
|
||||
<hkern g1="r" g2="comma,period,quotesinglbase,quotedblbase,ellipsis" k="173" />
|
||||
<hkern g1="r" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="19" />
|
||||
<hkern g1="r" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="30" />
|
||||
<hkern g1="x" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="40" />
|
||||
<hkern g1="x" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="20" />
|
||||
<hkern g1="z" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="16" />
|
||||
<hkern g1="z" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="16" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="o,ograve,oacute,ocircumflex,otilde,odieresis" k="91" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="m,n,p,ntilde" k="20" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="37" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Aring" k="120" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="c,d,e,g,q,ccedilla,egrave,eacute,ecircumflex,edieresis,oe" k="59" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="s" k="92" />
|
||||
<hkern g1="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" g2="a,agrave,aacute,acircumflex,atilde,adieresis,aring" k="50" />
|
||||
<hkern g1="comma,period,quotesinglbase,quotedblbase,ellipsis" g2="quotedbl,quotesingle,quoteleft,quoteright,quotedblleft,quotedblright" k="285" />
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
Before Width: | Height: | Size: 75 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user