mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-09 11:19:38 +03:00
Compare commits
187 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
2af0021c2b | ||
|
|
0f2eaccb39 | ||
|
|
b251671e3f | ||
|
|
c4eed37d8e | ||
|
|
8b43b91057 | ||
|
|
91fe7f0bee | ||
|
|
5cfb7b8de4 | ||
|
|
6329a1842a | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
393f6047f2 | ||
|
|
d94fc5b694 | ||
|
|
f06770d91d | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
11960d9d3a | ||
|
|
bbd8fff021 | ||
|
|
ec17bd8608 | ||
|
|
0dbb8babee | ||
|
|
b14e9fc619 | ||
|
|
63c6d3478d | ||
|
|
781f0e7887 | ||
|
|
23e014cb25 | ||
|
|
5b64358ef1 | ||
|
|
56df64063d | ||
|
|
3f81eba13b | ||
|
|
7973412c29 | ||
|
|
f83de5f834 | ||
|
|
f2ceba978a | ||
|
|
96c074bb56 | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
f8a299caee | ||
|
|
437dce7756 | ||
|
|
f8ad820281 | ||
|
|
757f16a3b5 | ||
|
|
632ecc668f | ||
|
|
92d393537c | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
43d9d2eba7 | ||
|
|
baa260a03d | ||
|
|
b157a9927a | ||
|
|
b246a67e8a | ||
|
|
2d958e88bf | ||
|
|
07c7d5af17 | ||
|
|
42976ca48c | ||
|
|
d05e85efa9 | ||
|
|
547e117760 | ||
|
|
50a5d3c546 | ||
|
|
3fd82200cc | ||
|
|
7215392784 | ||
|
|
8a9a8dfae5 | ||
|
|
c44314def3 | ||
|
|
8b899a9cf0 | ||
|
|
0ebdfa4825 | ||
|
|
32a06f119b | ||
|
|
ec30864ce5 | ||
|
|
6bc72e157a | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
10418323ef | ||
|
|
a11d5245ec | ||
|
|
565033e0d4 | ||
|
|
c25ef18900 | ||
|
|
b0a63ba0cc | ||
|
|
7b6c88f17c | ||
|
|
361ba8b244 | ||
|
|
9baa96d41c | ||
|
|
e584b4926f | ||
|
|
bcd9c2044e | ||
|
|
d582e76fed | ||
|
|
991dd8a558 | ||
|
|
bc49784797 | ||
|
|
7f99903fdb | ||
|
|
97d011ac8e | ||
|
|
61596a8e21 | ||
|
|
44e337cef6 | ||
|
|
1bec3eaa1e | ||
|
|
5942d796b5 | ||
|
|
eec9c05518 | ||
|
|
246d1621f5 | ||
|
|
6c1e06bf86 | ||
|
|
3c1e165134 | ||
|
|
80d1c594cc | ||
|
|
947db95d16 | ||
|
|
5b9362ab0b | ||
|
|
f602b088ac | ||
|
|
4acf0c4ee0 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
9d3f329bc9 | ||
|
|
bd00a03e7b | ||
|
|
be517de7dc | ||
|
|
23ab1f0c81 | ||
|
|
49621e7b15 | ||
|
|
1ab6f017c9 | ||
|
|
0b364fd72f | ||
|
|
e80ae76856 | ||
|
|
eebad3e2a0 | ||
|
|
db2af47286 | ||
|
|
7ad28aeab4 | ||
|
|
8d80e7311c | ||
|
|
7932069535 | ||
|
|
78564ec61d | ||
|
|
b80184cd93 | ||
|
|
1fa079b466 | ||
|
|
fcfb9470c9 | ||
|
|
c99653f0f2 | ||
|
|
5080b4996e | ||
|
|
1903422113 | ||
|
|
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 |
@@ -3,6 +3,10 @@ APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# The below url has to be set if using social auth options
|
||||
# or if you are not using BookStack at the root path of your domain.
|
||||
# APP_URL=http://bookstack.dev
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=database_database
|
||||
@@ -42,8 +46,6 @@ GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
11
.github/ISSUE_TEMPLATE.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
### For Feature Requests
|
||||
Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
PHP Version:
|
||||
|
||||
MySQL Version:
|
||||
|
||||
Expected Behavior:
|
||||
|
||||
Actual Behavior:
|
||||
31
.travis.yml
Normal file
31
.travis.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- mysql-server-5.6
|
||||
- mysql-client-core-5.6
|
||||
- mysql-client-5.6
|
||||
|
||||
before_script:
|
||||
- mysql -u root -e 'create database `bookstack-test`;'
|
||||
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer self-update
|
||||
- composer dump-autoload --no-interaction
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- php artisan clear-compiled -n
|
||||
- php artisan optimize -n
|
||||
- php artisan migrate --force -n --database=mysql_testing
|
||||
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
||||
|
||||
script:
|
||||
- phpunit
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Dan Brown
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string key
|
||||
* @property \User user
|
||||
@@ -28,7 +26,7 @@ class Activity extends Model
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +44,7 @@ class Activity extends Model
|
||||
* @return bool
|
||||
*/
|
||||
public function isSimilarTo($activityB) {
|
||||
return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id];
|
||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
40
app/Book.php
40
app/Book.php
@@ -1,35 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
return '/books/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . $this->slug . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . $this->slug);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany('BookStack\Page');
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
{
|
||||
return $this->hasMany('BookStack\Chapter');
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,25 +5,47 @@ class Chapter extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pages that this chapter contains.
|
||||
* @return mixed
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC');
|
||||
return $this->hasMany(Page::class)->orderBy('priority', 'ASC');
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url of this chapter.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
return '/books/' . $bookSlug. '/chapter/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this chapter's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
51
app/Console/Commands/RegeneratePermissions.php
Normal file
51
app/Console/Commands/RegeneratePermissions.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'permissions:regen';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Regenerate all system permissions';
|
||||
|
||||
/**
|
||||
* The service to handle the permission system.
|
||||
*
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->permissionService->buildJointPermissions();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\Inspire::class,
|
||||
\BookStack\Console\Commands\ResetViews::class,
|
||||
\BookStack\Console\Commands\RegeneratePermissions::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class EmailConfirmation extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'token'];
|
||||
|
||||
/**
|
||||
* Get the user that this confirmation is attached to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
114
app/Entity.php
114
app/Entity.php
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
abstract class Entity extends Ownable
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function activity()
|
||||
{
|
||||
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
|
||||
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,15 +51,24 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function views()
|
||||
{
|
||||
return $this->morphMany('BookStack\View', 'viewable');
|
||||
return $this->morphMany(View::class, 'viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tag models that have been user assigned to this entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
public function restrictions()
|
||||
public function permissions()
|
||||
{
|
||||
return $this->morphMany('BookStack\Restriction', 'restrictable');
|
||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +79,28 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function hasRestriction($role_id, $action)
|
||||
{
|
||||
return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
|
||||
return $this->permissions()->where('role_id', '=', $role_id)
|
||||
->where('action', '=', $action)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this entity has live (active) restrictions in place.
|
||||
* @param $role_id
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasActiveRestriction($role_id, $action)
|
||||
{
|
||||
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity jointPermissions this is connected to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->morphMany(JointPermission::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +111,32 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return static::getClassName() === strtolower($type);
|
||||
return static::getType() === strtolower($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity type.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
return strtolower(static::getClassName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of an entity of the given type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\' . $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,24 +157,31 @@ abstract class Entity extends Ownable
|
||||
* @param string[] array $wheres
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$exactTerms = [];
|
||||
$fuzzyTerms = [];
|
||||
$search = static::newQuery();
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/\s/', $term)) {
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
$term = '"' . $term . '"';
|
||||
$safeTerm = htmlentities($term, ENT_QUOTES);
|
||||
$safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm);
|
||||
if (preg_match('/".*?"/', $safeTerm) || is_numeric($safeTerm)) {
|
||||
$safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
|
||||
$exactTerms[] = '%' . $safeTerm . '%';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
$safeTerm = '' . $safeTerm . '*';
|
||||
if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
|
||||
}
|
||||
if ($term !== '*') $terms[$key] = $term;
|
||||
}
|
||||
$termString = implode(' ', $terms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
$isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
|
||||
|
||||
// Perform fulltext search if relevant terms exist.
|
||||
if ($isFuzzy) {
|
||||
$termString = implode(' ', $fuzzyTerms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
}
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
@@ -131,25 +193,21 @@ abstract class Entity extends Ownable
|
||||
}
|
||||
});
|
||||
}
|
||||
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if (static::isA('page')) {
|
||||
if ($this->isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if (static::isA('chapter')) {
|
||||
} else if ($this->isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy('title_relevance', 'desc');
|
||||
return $search->orderBy($orderBy, 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this item.
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getUrl();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Restriction extends Model
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['role_id', 'action'];
|
||||
@@ -16,6 +13,6 @@ class Restriction extends Model
|
||||
*/
|
||||
public function restrictable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
return $this->morphTo('restrictable');
|
||||
}
|
||||
}
|
||||
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,19 +47,44 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($e instanceof NotifyException) {
|
||||
\Session::flash('error', $e->message);
|
||||
return response()->redirectTo($e->redirectLocation);
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
session()->flash('error', $this->getOriginalMessage($e));
|
||||
return redirect($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
||||
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
* @param Exception $e
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type) {
|
||||
do {
|
||||
if (is_a($e, $type)) return true;
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original exception message.
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e) {
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
class PrettyException extends \Exception {}
|
||||
@@ -1,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,7 +33,6 @@ class AuthController extends Controller
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
protected $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();
|
||||
}
|
||||
@@ -115,6 +115,7 @@ class AuthController extends Controller
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,14 +194,11 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
$newUser->email_confirmed = false;
|
||||
$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());
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Request;
|
||||
use Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
@@ -29,4 +31,46 @@ class PasswordController extends Controller
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a reset link to the given user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function sendResetLinkEmail(Request $request)
|
||||
{
|
||||
$this->validate($request, ['email' => 'required|email']);
|
||||
|
||||
$broker = $this->getBroker();
|
||||
|
||||
$response = Password::broker($broker)->sendResetLink(
|
||||
$request->only('email'), $this->resetEmailBuilder()
|
||||
);
|
||||
|
||||
switch ($response) {
|
||||
case Password::RESET_LINK_SENT:
|
||||
$message = 'A password reset link has been sent to ' . $request->get('email') . '.';
|
||||
session()->flash('success', $message);
|
||||
return $this->getSendResetLinkEmailSuccessResponse($response);
|
||||
|
||||
case Password::INVALID_USER:
|
||||
default:
|
||||
return $this->getSendResetLinkEmailFailureResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for after a successful password reset.
|
||||
*
|
||||
* @param string $response
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function getResetSuccessResponse($response)
|
||||
{
|
||||
$message = 'Your password has been successfully reset.';
|
||||
session()->flash('success', $message);
|
||||
return redirect($this->redirectPath())->with('status', trans($response));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +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;
|
||||
@@ -40,7 +35,6 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
@@ -54,7 +48,6 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for creating a new book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function create()
|
||||
@@ -77,24 +70,20 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->bookRepo->newFromInput($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name);
|
||||
$book->created_by = Auth::user()->id;
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->bookRepo->createFromInput($request->all());
|
||||
Activity::add($book, 'book_create', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
@@ -103,7 +92,6 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
@@ -117,7 +105,6 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified book in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $slug
|
||||
* @return Response
|
||||
@@ -130,10 +117,7 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book->fill($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->bookRepo->updateFromInput($book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -160,7 +144,7 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->bookRepo->getChildren($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]);
|
||||
@@ -195,21 +179,31 @@ class BookController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
$sortedBooks = [];
|
||||
// Sort pages and chapters
|
||||
$sortedBooks = [];
|
||||
$updatedModels = collect();
|
||||
$sortMap = json_decode($request->get('sort-tree'));
|
||||
$defaultBookId = $book->id;
|
||||
foreach ($sortMap as $index => $bookChild) {
|
||||
$id = $bookChild->id;
|
||||
|
||||
// Loop through contents of provided map and update entities accordingly
|
||||
foreach ($sortMap as $bookChild) {
|
||||
$priority = $bookChild->sort;
|
||||
$id = intval($bookChild->id);
|
||||
$isPage = $bookChild->type == 'page';
|
||||
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
|
||||
$bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
|
||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||
$model->priority = $index;
|
||||
if ($isPage) {
|
||||
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
|
||||
|
||||
// Update models only if there's a change in parent chain or ordering.
|
||||
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||
$model->priority = $priority;
|
||||
if ($isPage) $model->chapter_id = $chapterId;
|
||||
$model->save();
|
||||
$updatedModels->push($model);
|
||||
}
|
||||
$model->save();
|
||||
|
||||
// Store involved books to be sorted later
|
||||
if (!in_array($bookId, $sortedBooks)) {
|
||||
$sortedBooks[] = $bookId;
|
||||
}
|
||||
@@ -221,6 +215,9 @@ class BookController extends Controller
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
|
||||
// Update permissions on changed models
|
||||
$this->bookRepo->buildJointPermissions($updatedModels);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -235,7 +232,7 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
Activity::addMessage('book_delete', 0, $book->name);
|
||||
Activity::removeEntity($book);
|
||||
$this->bookRepo->destroyBySlug($bookSlug);
|
||||
$this->bookRepo->destroy($book);
|
||||
return redirect('/books');
|
||||
}
|
||||
|
||||
@@ -266,8 +263,8 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
|
||||
session()->flash('success', 'Page Restrictions Updated');
|
||||
$this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
|
||||
session()->flash('success', 'Book Restrictions Updated');
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +57,9 @@ class ChapterController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
|
||||
$chapter = $this->chapterRepo->newFromInput($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
|
||||
$chapter->priority = $this->bookRepo->getNewPriority($book);
|
||||
$chapter->created_by = auth()->user()->id;
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$book->chapters()->save($chapter);
|
||||
$input = $request->all();
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($book);
|
||||
$chapter = $this->chapterRepo->createFromInput($input, $book);
|
||||
Activity::add($chapter, 'chapter_create', $book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
@@ -77,6 +74,7 @@ class ChapterController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($chapter);
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
@@ -156,6 +154,63 @@ class ChapterController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for moving a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $chapterSlug) {
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
return view('chapters/move', [
|
||||
'chapter' => $chapter,
|
||||
'book' => $book
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the move action for a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
$parent = false;
|
||||
|
||||
if ($entityType == 'book') {
|
||||
$parent = $this->bookRepo->getById($entityId);
|
||||
}
|
||||
|
||||
if ($parent === false || $parent === null) {
|
||||
session()->flash('The selected Book was not found');
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->chapterRepo->changeBook($parent->id, $chapter, true);
|
||||
Activity::add($chapter, 'chapter_move', $chapter->book->id);
|
||||
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
@@ -186,8 +241,8 @@ class ChapterController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
|
||||
session()->flash('success', 'Page Restrictions Updated');
|
||||
$this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
|
||||
session()->flash('success', 'Chapter Restrictions Updated');
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,4 +110,15 @@ abstract class Controller extends BaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send back a json error message.
|
||||
* @param string $messageText
|
||||
* @param int $statusCode
|
||||
* @return mixed
|
||||
*/
|
||||
protected function jsonError($messageText = "", $statusCode = 500)
|
||||
{
|
||||
return response()->json(['message' => $messageText], $statusCode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,14 +29,17 @@ class HomeController extends Controller
|
||||
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,10 +68,30 @@ 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
|
||||
*/
|
||||
@@ -66,13 +99,14 @@ class ImageController extends Controller
|
||||
{
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => 'image|mimes:jpeg,gif,png'
|
||||
'file' => 'is_image'
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -98,7 +132,7 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Update image details
|
||||
* @param $imageId
|
||||
* @param integer $imageId
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
@@ -113,12 +147,11 @@ class ImageController extends Controller
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,7 @@ use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
@@ -49,33 +50,59 @@ class PageController extends Controller
|
||||
public function create($bookSlug, $chapterSlug = false)
|
||||
{
|
||||
$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->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;
|
||||
$parent = $chapterId !== null ? $this->chapterRepo->getById($chapterId) : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
$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());
|
||||
@@ -101,12 +128,25 @@ class PageController extends Controller
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
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
|
||||
@@ -119,6 +159,26 @@ class PageController extends Controller
|
||||
$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]);
|
||||
}
|
||||
|
||||
@@ -132,7 +192,7 @@ class PageController extends Controller
|
||||
public function update(Request $request, $bookSlug, $pageSlug)
|
||||
{
|
||||
$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);
|
||||
@@ -142,6 +202,31 @@ class PageController extends Controller
|
||||
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.
|
||||
@@ -169,9 +254,25 @@ class PageController extends Controller
|
||||
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
|
||||
@@ -183,6 +284,24 @@ class PageController extends Controller
|
||||
$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());
|
||||
}
|
||||
@@ -248,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'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -265,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'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -282,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'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -293,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
|
||||
@@ -306,7 +425,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Updated Pages',
|
||||
'pages' => $pages
|
||||
@@ -326,13 +445,74 @@ class PageController extends Controller
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('pages/restrictions', [
|
||||
'page' => $page,
|
||||
'page' => $page,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this page.
|
||||
* Show the view to choose a new parent to move a page into.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
return view('pages/move', [
|
||||
'book' => $book,
|
||||
'page' => $page
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the action of moving the location of a page
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
$parent = false;
|
||||
|
||||
if ($entityType == 'chapter') {
|
||||
$parent = $this->chapterRepo->getById($entityId);
|
||||
} else if ($entityType == 'book') {
|
||||
$parent = $this->bookRepo->getById($entityId);
|
||||
}
|
||||
|
||||
if ($parent === false || $parent === null) {
|
||||
session()->flash('The selected Book or Chapter was not found');
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->pageRepo->changePageParent($page, $parent);
|
||||
Activity::add($page, 'page_move', $page->book->id);
|
||||
session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
|
||||
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permissions for this page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param Request $request
|
||||
@@ -343,8 +523,8 @@ class PageController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->pageRepo->updateRestrictionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Restrictions Updated');
|
||||
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Permissions Updated');
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
@@ -62,11 +63,13 @@ class PermissionController extends Controller
|
||||
* Show the form for editing a user role.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function editRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
if ($role->hidden) throw new PermissionsException('This role cannot be edited');
|
||||
return view('settings/roles/edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Services\ViewService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
@@ -15,18 +15,21 @@ class SearchController extends Controller
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $viewService;
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param $pageRepo
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param ViewService $viewService
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->viewService = $viewService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -48,9 +51,9 @@ class SearchController extends Controller
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle('Search For ' . $searchTerm);
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -69,8 +72,8 @@ class SearchController extends Controller
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Page Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $pages,
|
||||
'title' => 'Page Search Results',
|
||||
'entities' => $pages,
|
||||
'title' => 'Page Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -89,8 +92,8 @@ class SearchController extends Controller
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Chapter Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $chapters,
|
||||
'title' => 'Chapter Search Results',
|
||||
'entities' => $chapters,
|
||||
'title' => 'Chapter Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -109,8 +112,8 @@ class SearchController extends Controller
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
|
||||
$this->setPageTitle('Book Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $books,
|
||||
'title' => 'Book Search Results',
|
||||
'entities' => $books,
|
||||
'title' => 'Book Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
@@ -134,4 +137,35 @@ class SearchController extends Controller
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search for a list of entities and return a partial HTML response of matching entities.
|
||||
* Returns the most popular entities if no search is provided.
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entities = collect();
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items());
|
||||
if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items());
|
||||
if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items());
|
||||
$entities = $entities->sortByDesc('title_relevance');
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
})->toArray();
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames);
|
||||
}
|
||||
|
||||
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,14 @@ class SettingController extends Controller
|
||||
{
|
||||
$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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
75
app/Http/Controllers/TagController.php
Normal file
75
app/Http/Controllers/TagController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\TagRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* TagController constructor.
|
||||
* @param $tagRepo
|
||||
*/
|
||||
public function __construct(TagRepo $tagRepo)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the Tags for a particular entity
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
|
||||
return response()->json($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tags for a particular entity.
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateForEntity($entityType, $entityId, Request $request)
|
||||
{
|
||||
$entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
|
||||
if ($entity === null) return $this->jsonError("Entity not found", 404);
|
||||
|
||||
$inputTags = $request->input('tags');
|
||||
$tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
|
||||
return response()->json([
|
||||
'tags' => $tags,
|
||||
'message' => 'Tags successfully updated'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from a given search term.
|
||||
* @param Request $request
|
||||
*/
|
||||
public function getNameSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from a given search term.
|
||||
* @param Request $request
|
||||
*/
|
||||
public function getValueSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$tagName = $request->has('name') ? $request->get('name') : false;
|
||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,14 +31,21 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Display a listing of the users.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$users = $this->userRepo->getAllUsers();
|
||||
$listDetails = [
|
||||
'order' => $request->has('order') ? $request->get('order') : 'asc',
|
||||
'search' => $request->has('search') ? $request->get('search') : '',
|
||||
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
|
||||
];
|
||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||
$this->setPageTitle('Users');
|
||||
return view('users/index', ['users' => $users]);
|
||||
$users->appends($listDetails);
|
||||
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +56,8 @@ class UserController extends Controller
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
return view('users/create', ['authMethod' => $authMethod]);
|
||||
$roles = $this->userRepo->getAssignableRoles();
|
||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +125,8 @@ class UserController extends Controller
|
||||
$user = $this->user->findOrFail($id);
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
|
||||
$roles = $this->userRepo->getAssignableRoles();
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +173,9 @@ class UserController extends Controller
|
||||
|
||||
$user->save();
|
||||
session()->flash('success', 'User successfully updated');
|
||||
return redirect('/settings/users');
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||
return redirect($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,11 +207,14 @@ class UserController extends Controller
|
||||
});
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
if ($this->userRepo->isOnlyAdmin($user)) {
|
||||
session()->flash('error', 'You cannot delete the only admin');
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
$this->userRepo->destroy($user);
|
||||
session()->flash('success', 'User successfully removed');
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
@@ -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('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
return redirect()->guest('/login');
|
||||
return redirect()->guest(baseUrl('/login'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,25 +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}/restrict', 'BookController@showRestrict');
|
||||
Route::put('/{bookSlug}/restrict', 'BookController@restrict');
|
||||
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}/page/{pageSlug}/restrict', 'PageController@showRestrict');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
|
||||
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');
|
||||
@@ -50,9 +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}/restrict', 'ChapterController@showRestrict');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@restrict');
|
||||
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');
|
||||
|
||||
@@ -72,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');
|
||||
|
||||
|
||||
24
app/JointPermission.php
Normal file
24
app/JointPermission.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the role that this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphOne(Entity::class, 'entity');
|
||||
}
|
||||
}
|
||||
19
app/Model.php
Normal file
19
app/Model.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
||||
|
||||
class Model extends EloquentModel
|
||||
{
|
||||
|
||||
/**
|
||||
* Provides public access to get the raw attribute value from the model.
|
||||
* Used in areas where no mutations are required but performance is critical.
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRawAttribute($key)
|
||||
{
|
||||
return parent::getAttributeFromArray($key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
@@ -10,7 +9,7 @@ abstract class Ownable extends Model
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,7 +18,7 @@ abstract class Ownable extends Model
|
||||
*/
|
||||
public function updatedBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'updated_by');
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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,10 +1,6 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\User;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -15,7 +11,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
// Custom validation methods
|
||||
\Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
|
||||
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
|
||||
return in_array($value->getMimeType(), $imageMimes);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use BookStack\Services\LdapService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
|
||||
return new LdapUserProvider($config['model'], $app[LdapService::class]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\ViewService;
|
||||
use BookStack\Setting;
|
||||
use BookStack\View;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\Services\ActivityService;
|
||||
use BookStack\Services\SettingService;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
@@ -29,30 +36,30 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
return new ActivityService(
|
||||
$this->app->make('BookStack\Activity'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
$this->app->make(Activity::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
return new ViewService(
|
||||
$this->app->make('BookStack\View'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
$this->app->make(View::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('setting', function() {
|
||||
return new SettingService(
|
||||
$this->app->make('BookStack\Setting'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(Setting::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('images', function() {
|
||||
return new ImageService(
|
||||
$this->app->make('Intervention\Image\ImageManager'),
|
||||
$this->app->make('Illuminate\Contracts\Filesystem\Factory'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(ImageManager::class),
|
||||
$this->app->make(Factory::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,6 +1,8 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Alpha\B;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
use Views;
|
||||
@@ -29,7 +31,7 @@ class BookRepo extends EntityRepo
|
||||
*/
|
||||
private function bookQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book, 'view');
|
||||
return $this->permissionService->enforceBookRestrictions($this->book, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,21 +125,43 @@ class BookRepo extends EntityRepo
|
||||
|
||||
/**
|
||||
* Get a new book instance from request input.
|
||||
* @param $input
|
||||
* @param array $input
|
||||
* @return Book
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
public function createFromInput($input)
|
||||
{
|
||||
return $this->book->newInstance($input);
|
||||
$book = $this->book->newInstance($input);
|
||||
$book->slug = $this->findSuitableSlug($book->name);
|
||||
$book->created_by = auth()->user()->id;
|
||||
$book->updated_by = auth()->user()->id;
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a book identified by the given slug.
|
||||
* @param $bookSlug
|
||||
* Update the given book from user input.
|
||||
* @param Book $book
|
||||
* @param $input
|
||||
* @return Book
|
||||
*/
|
||||
public function destroyBySlug($bookSlug)
|
||||
public function updateFromInput(Book $book, $input)
|
||||
{
|
||||
$book->fill($input);
|
||||
$book->slug = $this->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = auth()->user()->id;
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the given book.
|
||||
* @param Book $book
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy(Book $book)
|
||||
{
|
||||
$book = $this->getBySlug($bookSlug);
|
||||
foreach ($book->pages as $page) {
|
||||
$this->pageRepo->destroy($page);
|
||||
}
|
||||
@@ -145,7 +169,8 @@ class BookRepo extends EntityRepo
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->restrictions()->delete();
|
||||
$book->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
@@ -183,12 +208,10 @@ class BookRepo extends EntityRepo
|
||||
*/
|
||||
public function findSuitableSlug($name, $currentId = false)
|
||||
{
|
||||
$originalSlug = Str::slug($name);
|
||||
$slug = $originalSlug;
|
||||
$count = 2;
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
while ($this->doesSlugExist($slug, $currentId)) {
|
||||
$slug = $originalSlug . '-' . $count;
|
||||
$count++;
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
@@ -196,32 +219,54 @@ class BookRepo extends EntityRepo
|
||||
/**
|
||||
* Get all child objects of a book.
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
|
||||
* Loads the book slug onto child elements to prevent access database access for getting the slug.
|
||||
* @param Book $book
|
||||
* @param bool $filterDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChildren(Book $book)
|
||||
public function getChildren(Book $book, $filterDrafts = false)
|
||||
{
|
||||
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
|
||||
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
$pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
|
||||
if ($filterDrafts) {
|
||||
$pageQuery = $pageQuery->where('draft', '=', false);
|
||||
}
|
||||
|
||||
$pages = $pageQuery->get();
|
||||
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function($query) {
|
||||
$this->restrictionService->enforcePageRestrictions($query, 'view');
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
|
||||
$this->permissionService->enforcePageRestrictions($query, 'view');
|
||||
if ($filterDrafts) $query->where('draft', '=', false);
|
||||
}]);
|
||||
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapters = $chapterQuery->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$children = $pages->values();
|
||||
foreach ($chapters as $chapter) {
|
||||
$children->push($chapter);
|
||||
}
|
||||
$bookSlug = $book->slug;
|
||||
|
||||
$children->each(function ($child) use ($bookSlug) {
|
||||
$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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,8 +279,9 @@ class BookRepo extends EntityRepo
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
|
||||
$bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
|
||||
$books = $bookQuery->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
|
||||
@@ -2,19 +2,32 @@
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
||||
class ChapterRepo extends EntityRepo
|
||||
{
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* Base query for getting chapters, Takes restrictions into account.
|
||||
* ChapterRepo constructor.
|
||||
* @param $pageRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting chapters, Takes permissions into account.
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,17 +79,30 @@ class ChapterRepo extends EntityRepo
|
||||
*/
|
||||
public function getChildren(Chapter $chapter)
|
||||
{
|
||||
return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
|
||||
$pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
|
||||
// Sort items with drafts first then by priority.
|
||||
return $pages->sortBy(function ($child, $key) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +119,8 @@ class ChapterRepo extends EntityRepo
|
||||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->restrictions()->delete();
|
||||
$chapter->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
@@ -124,12 +151,25 @@ class ChapterRepo extends EntityRepo
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
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 string $term
|
||||
@@ -141,8 +181,9 @@ class ChapterRepo extends EntityRepo
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
|
||||
$chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
|
||||
$chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
@@ -154,19 +195,32 @@ class ChapterRepo extends EntityRepo
|
||||
|
||||
/**
|
||||
* Changes the book relation of this chapter.
|
||||
* @param $bookId
|
||||
* @param $bookId
|
||||
* @param Chapter $chapter
|
||||
* @param bool $rebuildPermissions
|
||||
* @return Chapter
|
||||
*/
|
||||
public function changeBook($bookId, Chapter $chapter)
|
||||
public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
|
||||
{
|
||||
$chapter->book_id = $bookId;
|
||||
// Update related activity
|
||||
foreach ($chapter->activity as $activity) {
|
||||
$activity->book_id = $bookId;
|
||||
$activity->save();
|
||||
}
|
||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
|
||||
$chapter->save();
|
||||
// Update all child pages
|
||||
foreach ($chapter->pages as $page) {
|
||||
$this->pageRepo->changeBook($bookId, $page);
|
||||
}
|
||||
|
||||
// Update permissions if applicable
|
||||
if ($rebuildPermissions) {
|
||||
$chapter->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
@@ -25,9 +28,15 @@ class EntityRepo
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var RestrictionService
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
@@ -37,7 +46,7 @@ class EntityRepo
|
||||
$this->book = app(Book::class);
|
||||
$this->chapter = app(Chapter::class);
|
||||
$this->page = app(Page::class);
|
||||
$this->restrictionService = app(RestrictionService::class);
|
||||
$this->permissionService = app(PermissionService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +58,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforceBookRestrictions($this->book)
|
||||
$query = $this->permissionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
@@ -65,7 +74,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book)
|
||||
return $this->permissionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
@@ -78,12 +87,12 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforcePageRestrictions($this->page)
|
||||
->orderBy('created_at', 'desc');
|
||||
$query = $this->permissionService->enforcePageRestrictions($this->page)
|
||||
->orderBy('created_at', 'desc')->where('draft', '=', false);
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
return $query->with('book')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +104,7 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforceChapterRestrictions($this->chapter)
|
||||
$query = $this->permissionService->enforceChapterRestrictions($this->chapter)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
@@ -111,8 +120,23 @@ class EntityRepo
|
||||
*/
|
||||
public function getRecentlyUpdatedPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->restrictionService->enforcePageRestrictions($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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,14 +144,14 @@ class EntityRepo
|
||||
* @param $request
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function updateRestrictionsFromRequest($request, Entity $entity)
|
||||
public function updateEntityPermissionsFromRequest($request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
|
||||
$entity->restrictions()->delete();
|
||||
$entity->permissions()->delete();
|
||||
if ($request->has('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->restrictions()->create([
|
||||
$entity->permissions()->create([
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action)
|
||||
]);
|
||||
@@ -135,6 +159,7 @@ class EntityRepo
|
||||
}
|
||||
}
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,18 +169,117 @@ class EntityRepo
|
||||
* @param $termString
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareSearchTerms($termString)
|
||||
public function prepareSearchTerms($termString)
|
||||
{
|
||||
preg_match_all('/"(.*?)"/', $termString, $matches);
|
||||
$termString = $this->cleanSearchTermString($termString);
|
||||
preg_match_all('/(".*?")/', $termString, $matches);
|
||||
$terms = [];
|
||||
if (count($matches[1]) > 0) {
|
||||
$terms = $matches[1];
|
||||
foreach ($matches[1] as $match) {
|
||||
$terms[] = $match;
|
||||
}
|
||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||
} else {
|
||||
$terms = [];
|
||||
}
|
||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any special search notation that should not
|
||||
* be used in a full-text search.
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function cleanSearchTermString($termString)
|
||||
{
|
||||
// Strip tag searches
|
||||
$termString = preg_replace('/\[.*?\]/', '', $termString);
|
||||
// Reduced multiple spacing into single spacing
|
||||
$termString = preg_replace("/\s{2,}/", " ", $termString);
|
||||
return $termString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses advanced search notations and adds them to the db query.
|
||||
* @param $query
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function addAdvancedSearchQueries($query, $termString)
|
||||
{
|
||||
$escapedOperators = $this->getRegexEscapedOperators();
|
||||
// Look for tag searches
|
||||
preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
|
||||
if (count($tags[0]) > 0) {
|
||||
$this->applyTagSearches($query, $tags);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply extracted tag search terms onto a entity query.
|
||||
* @param $query
|
||||
* @param $tags
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearches($query, $tags) {
|
||||
$query->where(function($query) use ($tags) {
|
||||
foreach ($tags[1] as $index => $tagName) {
|
||||
$query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
|
||||
$tagOperator = $tags[3][$index];
|
||||
$tagValue = $tags[4][$index];
|
||||
if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias method to update the book jointPermissions in the PermissionService.
|
||||
* @param Collection $collection collection on entities
|
||||
*/
|
||||
public function buildJointPermissions(Collection $collection)
|
||||
{
|
||||
$this->permissionService->buildJointPermissionsForEntities($collection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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,44 +1,57 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
|
||||
class PageRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $pageRevision;
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
* @param PageRevision $pageRevision
|
||||
* @param TagRepo $tagRepo
|
||||
*/
|
||||
public function __construct(PageRevision $pageRevision)
|
||||
public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
|
||||
{
|
||||
$this->pageRevision = $pageRevision;
|
||||
$this->tagRepo = $tagRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting pages, Takes restrictions into account.
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
private function pageQuery()
|
||||
private function pageQuery($allowDrafts = false)
|
||||
{
|
||||
return $this->restrictionService->enforcePageRestrictions($this->page, 'view');
|
||||
$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->pageQuery()->findOrFail($id);
|
||||
return $this->pageQuery($allowDrafts)->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,9 +79,10 @@ class PageRepo extends EntityRepo
|
||||
public function findPageUsingOldSlug($pageSlug, $bookSlug)
|
||||
{
|
||||
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function($query) {
|
||||
$this->restrictionService->enforcePageRestrictions($query);
|
||||
->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;
|
||||
@@ -100,8 +114,8 @@ class PageRepo extends EntityRepo
|
||||
* 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)
|
||||
@@ -120,6 +134,55 @@ class PageRepo extends EntityRepo
|
||||
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.
|
||||
@@ -128,9 +191,9 @@ class PageRepo extends EntityRepo
|
||||
*/
|
||||
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;
|
||||
@@ -190,8 +253,9 @@ class PageRepo extends EntityRepo
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
|
||||
$pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
|
||||
$pages = $pageQuery->paginate($count)->appends($paginationAppends);
|
||||
|
||||
// Add highlights to page text.
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
@@ -239,29 +303,44 @@ class PageRepo extends EntityRepo
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -287,16 +366,20 @@ class PageRepo extends EntityRepo
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
* @param null|string $summary
|
||||
* @return $this
|
||||
*/
|
||||
public function saveRevision(Page $page)
|
||||
public function saveRevision(Page $page, $summary = null)
|
||||
{
|
||||
$revision = $this->pageRevision->fill($page->toArray());
|
||||
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
||||
$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) {
|
||||
@@ -306,6 +389,155 @@ class PageRepo extends EntityRepo
|
||||
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
|
||||
@@ -333,7 +565,7 @@ class PageRepo extends EntityRepo
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -349,16 +581,33 @@ class PageRepo extends EntityRepo
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
* @param Page $page
|
||||
* @param Entity $parent
|
||||
*/
|
||||
public function changePageParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = $parent->isA('book') ? $parent : $parent->book;
|
||||
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
|
||||
$page->save();
|
||||
$page = $this->changeBook($book->id, $page);
|
||||
$page->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a suitable slug for the resource
|
||||
* @param $name
|
||||
* @param $bookId
|
||||
* @param string $name
|
||||
* @param int $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
@@ -369,12 +618,14 @@ class PageRepo extends EntityRepo
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param $page
|
||||
*/
|
||||
public function destroy($page)
|
||||
public function destroy(Page $page)
|
||||
{
|
||||
Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->tags()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->restrictions()->delete();
|
||||
$page->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Permission;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
@@ -11,16 +12,21 @@ class PermissionsRepo
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
|
||||
protected $systemRoles = ['admin', 'public'];
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param $permission
|
||||
* @param $role
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Permission $permission, Role $role)
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +35,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->all();
|
||||
return $this->role->where('hidden', '=', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +45,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role)
|
||||
{
|
||||
return $this->role->where('id', '!=', $role->id)->get();
|
||||
return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +75,7 @@ class PermissionsRepo
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
return $role;
|
||||
}
|
||||
|
||||
@@ -77,10 +84,14 @@ class PermissionsRepo
|
||||
* Ensure Admin role always has all permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function updateRole($roleId, $roleData)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
@@ -91,6 +102,7 @@ class PermissionsRepo
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,8 +134,8 @@ class PermissionsRepo
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->name === 'admin') {
|
||||
throw new PermissionsException('The admin role cannot be deleted');
|
||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||
throw new PermissionsException('This role is a system role and cannot be deleted');
|
||||
} else if ($role->id == setting('registration-role')) {
|
||||
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
|
||||
}
|
||||
@@ -136,6 +148,7 @@ class PermissionsRepo
|
||||
}
|
||||
}
|
||||
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
|
||||
135
app/Repos/TagRepo.php
Normal file
135
app/Repos/TagRepo.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Tag;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Services\PermissionService;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class TagRepo
|
||||
{
|
||||
|
||||
protected $tag;
|
||||
protected $entity;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* TagRepo constructor.
|
||||
* @param Tag $attr
|
||||
* @param Entity $ent
|
||||
* @param PermissionService $ps
|
||||
*/
|
||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
||||
{
|
||||
$this->tag = $attr;
|
||||
$this->entity = $ent;
|
||||
$this->permissionService = $ps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance of its particular type.
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param string $action
|
||||
*/
|
||||
public function getEntity($entityType, $entityId, $action = 'view')
|
||||
{
|
||||
$entityInstance = $this->entity->getEntityInstance($entityType);
|
||||
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
|
||||
$searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
|
||||
return $searchQuery->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a particular entity.
|
||||
* @param string $entityType
|
||||
* @param int $entityId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$entity = $this->getEntity($entityType, $entityId);
|
||||
if ($entity === null) return collect();
|
||||
|
||||
return $entity->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from scanning existing tag names.
|
||||
* If no search term is given the 50 most popular tag names are provided.
|
||||
* @param $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function getNameSuggestions($searchTerm = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['name'])->pluck('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from scanning existing tag values.
|
||||
* If no search is given the 50 most popular values are provided.
|
||||
* Passing a tagName will only find values for a tags with a particular name.
|
||||
* @param $searchTerm
|
||||
* @param $tagName
|
||||
* @return array
|
||||
*/
|
||||
public function getValueSuggestions($searchTerm = false, $tagName = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['value'])->pluck('value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* @param Entity $entity
|
||||
* @param array $tags
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function saveTagsToEntity(Entity $entity, $tags = [])
|
||||
{
|
||||
$entity->tags()->delete();
|
||||
$newTags = [];
|
||||
foreach ($tags as $tag) {
|
||||
if (trim($tag['name']) === '') continue;
|
||||
$newTags[] = $this->newInstanceFromInput($tag);
|
||||
}
|
||||
|
||||
return $entity->tags()->saveMany($newTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Tag instance from user input.
|
||||
* @param $input
|
||||
* @return static
|
||||
*/
|
||||
protected function newInstanceFromInput($input)
|
||||
{
|
||||
$name = trim($input['name']);
|
||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||
// Any other modification or cleanup required can go here
|
||||
$values = ['name' => $name, 'value' => $value];
|
||||
return $this->tag->newInstance($values);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,27 @@ class UserRepo
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* @param int $count
|
||||
* @param $sortData
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
|
||||
{
|
||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
@@ -106,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
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -167,6 +189,15 @@ class UserRepo
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAssignableRoles()
|
||||
{
|
||||
return $this->role->visible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles which can be given restricted access to
|
||||
* other entities in the system.
|
||||
@@ -174,7 +205,7 @@ class UserRepo
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('name', '!=', 'admin')->get();
|
||||
return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
|
||||
}
|
||||
|
||||
}
|
||||
64
app/Role.php
64
app/Role.php
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
@@ -14,35 +11,58 @@ class Role extends Model
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\User');
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The permissions that belong to the role.
|
||||
* Get all related JointPermissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->hasMany(JointPermission::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The RolePermissions that belong to the role.
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permission');
|
||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role has a permission.
|
||||
* @param $permission
|
||||
* @param $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permission)
|
||||
public function hasPermission($permissionName)
|
||||
{
|
||||
return $this->permissions->pluck('name')->contains($permission);
|
||||
$permissions = $this->getRelationValue('permissions');
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->getRawAttribute('name') === $permissionName) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param Permission $permission
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function attachPermission(Permission $permission)
|
||||
public function attachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->attach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a single permission from this role.
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function detachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified role.
|
||||
* @param $roleName
|
||||
@@ -52,4 +72,24 @@ class Role extends Model
|
||||
{
|
||||
return static::where('name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified system role.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getSystemRole($roleName)
|
||||
{
|
||||
return static::where('system_name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* @return mixed
|
||||
*/
|
||||
public static function visible()
|
||||
{
|
||||
return static::where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permissions');
|
||||
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
* @param $roleName
|
||||
* @param $name
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
@@ -8,17 +8,17 @@ class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param Activity $activity
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity, RestrictionService $restrictionService)
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ class ActivityService
|
||||
*/
|
||||
public function latest($count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
@@ -105,8 +105,16 @@ class ActivityService
|
||||
*/
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
if ($entity->isA('book')) {
|
||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
||||
} else {
|
||||
$query = $this->activity->where('entity_type', '=', get_class($entity))
|
||||
->where('entity_id', '=', $entity->id);
|
||||
}
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activity);
|
||||
}
|
||||
@@ -121,7 +129,7 @@ class ActivityService
|
||||
*/
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
|
||||
return $this->filterSimilar($activityList);
|
||||
|
||||
@@ -48,11 +48,13 @@ class ExportService
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$pathString = public_path($srcString);
|
||||
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
||||
if ($isLocal) {
|
||||
$pathString = public_path(trim($srcString, '/'));
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
if ($isLocal && !file_exists($pathString)) continue;
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
|
||||
@@ -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,10 +75,11 @@ 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('app-secure-images');
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Entity;
|
||||
|
||||
class RestrictionService
|
||||
{
|
||||
|
||||
protected $userRoles;
|
||||
protected $isAdmin;
|
||||
protected $currentAction;
|
||||
|
||||
/**
|
||||
* RestrictionService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$user = auth()->user();
|
||||
$this->userRoles = $user ? auth()->user()->roles->pluck('id') : [];
|
||||
$this->isAdmin = $user ? auth()->user()->hasRole('admin') : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function checkIfEntityRestricted(Entity $entity, $action)
|
||||
{
|
||||
if ($this->isAdmin) return true;
|
||||
$this->currentAction = $action;
|
||||
$baseQuery = $entity->where('id', '=', $entity->id);
|
||||
if ($entity->isA('page')) {
|
||||
return $this->pageRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $this->bookRestrictionQuery($baseQuery)->count() > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a page query
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforcePageRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->pageRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting pages.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function pageRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where(function ($query) {
|
||||
$query->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', false);
|
||||
})->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
})->where('restricted', '=', false);
|
||||
})->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)->where('chapter_id', '=', 0)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has no chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id');
|
||||
}, 'and', true)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has a chapter with accepted permissions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', true)
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'pages', 'Page');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add on permission restrictions to a chapter query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceChapterRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->chapterRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting chapters.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// Book & chapter unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where('restricted', '=', false)->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
})
|
||||
// Chapter unrestricted & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Chapter has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions to a book query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceBookRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->bookRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting books.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
$parentWhereQuery
|
||||
->where('restricted', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', true)->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
return $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
|
||||
->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->bookRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->chapterRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The query to check the restrictions on an entity.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $modelName
|
||||
*/
|
||||
private function checkRestrictionsQuery($query, $tableName, $modelName)
|
||||
{
|
||||
$query->select('*')->from('restrictions')
|
||||
->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
|
||||
->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
|
||||
->where('restrictions.action', '=', $this->currentAction)
|
||||
->whereIn('restrictions.role_id', $this->userRoles);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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,20 +113,20 @@ class SocialAuthService
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
\Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
|
||||
session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||
\Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
|
||||
session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
// Change the user that the social account is assigned to.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
@@ -138,6 +135,7 @@ class SocialAuthService
|
||||
if (setting('registration-enabled')) {
|
||||
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
}
|
||||
|
||||
@@ -160,7 +158,7 @@ class SocialAuthService
|
||||
$driver = trim(strtolower($socialDriver));
|
||||
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
|
||||
|
||||
return $driver;
|
||||
}
|
||||
@@ -217,7 +215,7 @@ class SocialAuthService
|
||||
{
|
||||
session();
|
||||
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
\Session::flash('success', $socialDriver . ' account successfully detached');
|
||||
session()->flash('success', title_case($socialDriver) . ' account successfully detached');
|
||||
return redirect(auth()->user()->getEditUrl());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Entity;
|
||||
use BookStack\View;
|
||||
|
||||
@@ -9,18 +8,18 @@ class ViewService
|
||||
|
||||
protected $view;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(View $view, RestrictionService $restrictionService)
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,28 +46,27 @@ class ViewService
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false $filterModel
|
||||
* @param bool|false|array $filterModel
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
->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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,21 +79,18 @@ class ViewService
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
if ($this->user === null) return collect();
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->restrictionService
|
||||
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('user_id', '=', auth()->user()->id);
|
||||
|
||||
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable;
|
||||
});
|
||||
return $viewedEntities;
|
||||
$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.
|
||||
*/
|
||||
@@ -104,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');
|
||||
}
|
||||
}
|
||||
41
app/User.php
41
app/User.php
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
@@ -52,7 +49,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +113,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function socialAccounts()
|
||||
{
|
||||
return $this->hasMany('BookStack\SocialAccount');
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,8 +138,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
{
|
||||
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return '/user_avatar.png';
|
||||
return $this->avatar->getThumb($size, $size, false);
|
||||
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return baseUrl('/user_avatar.png');
|
||||
return baseUrl($this->avatar->getThumb($size, $size, false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +148,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Image', 'image_id');
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,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
|
||||
{
|
||||
|
||||
156
app/helpers.php
156
app/helpers.php
@@ -1,63 +1,58 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('versioned_asset')) {
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
function versioned_asset($file)
|
||||
{
|
||||
static $manifest = null;
|
||||
use BookStack\Ownable;
|
||||
|
||||
if (is_null($manifest)) {
|
||||
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
function versioned_asset($file = '')
|
||||
{
|
||||
// Don't require css and JS assets for testing
|
||||
if (config('app.env') === 'testing') return '';
|
||||
|
||||
static $manifest = null;
|
||||
$manifestPath = 'build/manifest.json';
|
||||
|
||||
if (is_null($manifest) && file_exists($manifestPath)) {
|
||||
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
|
||||
} else if (!file_exists($manifestPath)) {
|
||||
if (config('app.env') !== 'production') {
|
||||
$path = public_path($manifestPath);
|
||||
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
|
||||
} else {
|
||||
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
|
||||
}
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return '/' . $manifest[$file];
|
||||
}
|
||||
|
||||
if (file_exists(public_path($file))) {
|
||||
return '/' . $file;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||
throw new \Exception($error);
|
||||
}
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return baseUrl($manifest[$file]);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has a permission.
|
||||
* If an ownable element is passed in the permissions are checked against
|
||||
* If an ownable element is passed in the jointPermissions are checked against
|
||||
* that particular item.
|
||||
* @param $permission
|
||||
* @param \BookStack\Ownable $ownable
|
||||
* @param Ownable $ownable
|
||||
* @return mixed
|
||||
*/
|
||||
function userCan($permission, \BookStack\Ownable $ownable = null)
|
||||
function userCan($permission, Ownable $ownable = null)
|
||||
{
|
||||
if (!auth()->check()) return false;
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissionBaseName = strtolower($permission) . '-';
|
||||
$hasPermission = false;
|
||||
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
|
||||
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
|
||||
|
||||
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
|
||||
|
||||
// Check restrictions on the entitiy
|
||||
$restrictionService = app('BookStack\Services\RestrictionService');
|
||||
$explodedPermission = explode('-', $permission);
|
||||
$action = end($explodedPermission);
|
||||
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
|
||||
return $hasAccess && $hasPermission;
|
||||
$permissionService = app(\BookStack\Services\PermissionService::class);
|
||||
return $permissionService->checkOwnableUserAccess($ownable, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,3 +66,82 @@ function setting($key, $default = false)
|
||||
$settingService = app('BookStack\Services\SettingService');
|
||||
return $settingService->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create url's relative to the applications root path.
|
||||
* @param string $path
|
||||
* @param bool $forceAppDomain
|
||||
* @return string
|
||||
*/
|
||||
function baseUrl($path, $forceAppDomain = false)
|
||||
{
|
||||
$isFullUrl = strpos($path, 'http') === 0;
|
||||
if ($isFullUrl && !$forceAppDomain) return $path;
|
||||
$path = trim($path, '/');
|
||||
|
||||
if ($isFullUrl && $forceAppDomain) {
|
||||
$explodedPath = explode('/', $path);
|
||||
$path = implode('/', array_splice($explodedPath, 3));
|
||||
}
|
||||
|
||||
// Return normal url path if not specified in config
|
||||
if (config('app.url') === '') {
|
||||
return url($path);
|
||||
}
|
||||
|
||||
return rtrim(config('app.url'), '/') . '/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the redirector.
|
||||
* Overrides the default laravel redirect helper.
|
||||
* Ensures it redirects even when the app is in a subdirectory.
|
||||
*
|
||||
* @param string|null $to
|
||||
* @param int $status
|
||||
* @param array $headers
|
||||
* @param bool $secure
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
function redirect($to = null, $status = 302, $headers = [], $secure = null)
|
||||
{
|
||||
if (is_null($to)) {
|
||||
return app('redirect');
|
||||
}
|
||||
|
||||
$to = baseUrl($to);
|
||||
|
||||
return app('redirect')->to($to, $status, $headers, $secure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a url with multiple parameters for sorting purposes.
|
||||
* Works out the logic to set the correct sorting direction
|
||||
* Discards empty parameters and allows overriding.
|
||||
* @param $path
|
||||
* @param array $data
|
||||
* @param array $overrideData
|
||||
* @return string
|
||||
*/
|
||||
function sortUrl($path, $data, $overrideData = [])
|
||||
{
|
||||
$queryStringSections = [];
|
||||
$queryData = array_merge($data, $overrideData);
|
||||
|
||||
// Change sorting direction is already sorted on current attribute
|
||||
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
||||
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
||||
} else {
|
||||
$queryData['order'] = 'asc';
|
||||
}
|
||||
|
||||
foreach ($queryData as $name => $value) {
|
||||
$trimmedVal = trim($value);
|
||||
if ($trimmedVal === '') continue;
|
||||
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
|
||||
}
|
||||
|
||||
if (count($queryStringSections) === 0) return $path;
|
||||
|
||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||
}
|
||||
@@ -14,6 +14,7 @@ define('LARAVEL_START', microtime(true));
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../app/helpers.php';
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,9 +6,8 @@ if (env('CACHE_DRIVER') === 'memcached') {
|
||||
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
|
||||
foreach ($memcachedServers as $index => $memcachedServer) {
|
||||
$memcachedServerDetails = explode(':', $memcachedServer);
|
||||
$components = count($memcachedServerDetails);
|
||||
if ($components < 2) $memcachedServerDetails[] = '11211';
|
||||
if ($components < 3) $memcachedServerDetails[] = '100';
|
||||
if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
|
||||
if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
|
||||
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
|
||||
}
|
||||
}
|
||||
@@ -83,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 : [],
|
||||
|
||||
];
|
||||
|
||||
15
config/setting-defaults.php
Normal file
15
config/setting-defaults.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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)',
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
];
|
||||
@@ -52,4 +52,11 @@ $factory->define(BookStack\Role::class, function ($faker) {
|
||||
'display_name' => $faker->sentence(3),
|
||||
'description' => $faker->sentence(10)
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Tag::class, function ($faker) {
|
||||
return [
|
||||
'name' => $faker->city,
|
||||
'value' => $faker->sentence(3)
|
||||
];
|
||||
});
|
||||
@@ -21,10 +21,13 @@ class CreateUsersTable extends Migration
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
\BookStack\User::forceCreate([
|
||||
// Create the initial admin user
|
||||
DB::table('users')->insert([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => bcrypt('password')
|
||||
'password' => bcrypt('password'),
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ class CreateBooksTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('books', function (Blueprint $table) {
|
||||
$pdo = \DB::connection()->getPdo();
|
||||
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
|
||||
|
||||
Schema::create('books', function (Blueprint $table) use ($requiresISAM) {
|
||||
if($requiresISAM) $table->engine = 'MyISAM';
|
||||
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->string('slug')->indexed();
|
||||
|
||||
@@ -12,7 +12,13 @@ class CreatePagesTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('pages', function (Blueprint $table) {
|
||||
$pdo = \DB::connection()->getPdo();
|
||||
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
|
||||
|
||||
Schema::create('pages', function (Blueprint $table) use ($requiresISAM) {
|
||||
if($requiresISAM) $table->engine = 'MyISAM';
|
||||
|
||||
$table->increments('id');
|
||||
$table->integer('book_id');
|
||||
$table->integer('chapter_id');
|
||||
|
||||
@@ -12,7 +12,12 @@ class CreateChaptersTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('chapters', function (Blueprint $table) {
|
||||
$pdo = \DB::connection()->getPdo();
|
||||
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
|
||||
|
||||
Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) {
|
||||
if($requiresISAM) $table->engine = 'MyISAM';
|
||||
$table->increments('id');
|
||||
$table->integer('book_id');
|
||||
$table->string('slug')->indexed();
|
||||
|
||||
@@ -68,35 +68,44 @@ class AddRolesAndPermissions extends Migration
|
||||
|
||||
|
||||
// Create default roles
|
||||
$admin = new \BookStack\Role();
|
||||
$admin->name = 'admin';
|
||||
$admin->display_name = 'Admin';
|
||||
$admin->description = 'Administrator of the whole application';
|
||||
$admin->save();
|
||||
$adminId = DB::table('roles')->insertGetId([
|
||||
'name' => 'admin',
|
||||
'display_name' => 'Admin',
|
||||
'description' => 'Administrator of the whole application',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
$editorId = DB::table('roles')->insertGetId([
|
||||
'name' => 'editor',
|
||||
'display_name' => 'Editor',
|
||||
'description' => 'User can edit Books, Chapters & Pages',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
$viewerId = DB::table('roles')->insertGetId([
|
||||
'name' => 'viewer',
|
||||
'display_name' => 'Viewer',
|
||||
'description' => 'User can view books & their content behind authentication',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
|
||||
$editor = new \BookStack\Role();
|
||||
$editor->name = 'editor';
|
||||
$editor->display_name = 'Editor';
|
||||
$editor->description = 'User can edit Books, Chapters & Pages';
|
||||
$editor->save();
|
||||
|
||||
$viewer = new \BookStack\Role();
|
||||
$viewer->name = 'viewer';
|
||||
$viewer->display_name = 'Viewer';
|
||||
$viewer->description = 'User can view books & their content behind authentication';
|
||||
$viewer->save();
|
||||
|
||||
// Create default CRUD permissions and allocate to admins and editors
|
||||
$entities = ['Book', 'Page', 'Chapter', 'Image'];
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity . 's';
|
||||
$newPermission->save();
|
||||
$admin->attachPermission($newPermission);
|
||||
$editor->attachPermission($newPermission);
|
||||
$newPermId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
['permission_id' => $newPermId, 'role_id' => $adminId],
|
||||
['permission_id' => $newPermId, 'role_id' => $editorId]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +114,27 @@ class AddRolesAndPermissions extends Migration
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity;
|
||||
$newPermission->save();
|
||||
$admin->attachPermission($newPermission);
|
||||
$newPermId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'permission_id' => $newPermId,
|
||||
'role_id' => $adminId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set all current users as admins
|
||||
// (At this point only the initially create user should be an admin)
|
||||
$users = \BookStack\User::all();
|
||||
$users = DB::table('users')->get();
|
||||
foreach ($users as $user) {
|
||||
$user->attachRole($admin);
|
||||
DB::table('role_user')->insert([
|
||||
'role_id' => $adminId,
|
||||
'user_id' => $user->id
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,29 +13,31 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
public function up()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
$editorRole = \BookStack\Role::getRole('editor');
|
||||
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
|
||||
$editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
|
||||
|
||||
// Delete old permissions
|
||||
$permissions = \BookStack\Permission::all();
|
||||
$permissions->each(function ($permission) {
|
||||
$permission->delete();
|
||||
});
|
||||
$permissions = DB::table('permissions')->delete();
|
||||
|
||||
// Create & attach new admin permissions
|
||||
$permissionsToCreate = [
|
||||
'settings-manage' => 'Manage Settings',
|
||||
'users-manage' => 'Manage Users',
|
||||
'user-roles-manage' => 'Manage Roles & Permissions',
|
||||
'restrictions-manage-all' => 'Manage All Entity Restrictions',
|
||||
'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content'
|
||||
'restrictions-manage-all' => 'Manage All Entity Permissions',
|
||||
'restrictions-manage-own' => 'Manage Entity Permissions On Own Content'
|
||||
];
|
||||
foreach ($permissionsToCreate as $name => $displayName) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = $name;
|
||||
$newPermission->display_name = $displayName;
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => $name,
|
||||
'display_name' => $displayName,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
|
||||
// Create & attach new entity permissions
|
||||
@@ -43,12 +45,22 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
|
||||
$newPermission->display_name = $op . ' ' . $entity . 's';
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
if ($editorRole !== null) $editorRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
if ($editorRole !== null) {
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $editorRole->id,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,24 +74,26 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
public function down()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
|
||||
|
||||
// Delete old permissions
|
||||
$permissions = \BookStack\Permission::all();
|
||||
$permissions->each(function ($permission) {
|
||||
$permission->delete();
|
||||
});
|
||||
$permissions = DB::table('permissions')->delete();
|
||||
|
||||
// Create default CRUD permissions and allocate to admins and editors
|
||||
$entities = ['Book', 'Page', 'Chapter', 'Image'];
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity . 's';
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +102,16 @@ class UpdatePermissionsAndRoles extends Migration
|
||||
$ops = ['Create', 'Update', 'Delete'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
|
||||
$newPermission->display_name = $op . ' ' . $entity;
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
$permissionId = DB::table('permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower($op),
|
||||
'display_name' => $op . ' ' . $entity,
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,12 @@
|
||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||
<env name="MAIL_DRIVER" value="log"/>
|
||||
<env name="AUTH_METHOD" value="standard"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||
<env name="LDAP_VERSION" value="3"/>
|
||||
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="APP_URL" value="http://bookstack.dev"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"css/styles.css": "css/styles.css?version=c1c35d2",
|
||||
"css/print-styles.css": "css/print-styles.css?version=c1c35d2",
|
||||
"js/common.js": "js/common.js?version=c1c35d2"
|
||||
"css/styles.css": "css/styles.css?version=5be13a8",
|
||||
"css/print-styles.css": "css/print-styles.css?version=5be13a8",
|
||||
"js/common.js": "js/common.js?version=5be13a8"
|
||||
}
|
||||
2
public/css/export-styles.css
vendored
2
public/css/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/print-styles.css
vendored
2
public/css/print-styles.css
vendored
@@ -1 +1 @@
|
||||
.faded-small,.print-hidden,header{display:none}body{font-size:12px}.page-content{margin:0 auto}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
header{display:none}body{font-size:12px}.faded-small{display:none}.page-content{margin:0 auto}.print-hidden{display:none}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
2
public/css/styles.css
vendored
2
public/css/styles.css
vendored
File diff suppressed because one or more lines are too long
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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user