mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-09 19:06:13 +03:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
8933179017 | ||
|
|
792e786880 | ||
|
|
753f6394f7 | ||
|
|
0a8030306e | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
d6bad01130 | ||
|
|
a33deed26b | ||
|
|
2e7345f4f0 | ||
|
|
1a7de4c2d6 | ||
|
|
66ba773367 | ||
|
|
afc3583be8 | ||
|
|
cbff2c6035 | ||
|
|
8e614ecb6e | ||
|
|
d099885fd1 | ||
|
|
2bb8c3d914 | ||
|
|
4caa61fe96 | ||
|
|
c5960f9b6a | ||
|
|
412eed19c3 | ||
|
|
e9b596d3bc | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
8b109bac13 | ||
|
|
097d9c9f3c | ||
|
|
e7d8a041a8 | ||
|
|
dc2978824e | ||
|
|
e1994ef2cf | ||
|
|
efb49019d4 | ||
|
|
ef874712bb | ||
|
|
26965fa08f | ||
|
|
1fe933e4ea | ||
|
|
491f73e0cd | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
4656c12f6d | ||
|
|
a06321675a | ||
|
|
dbe11c1360 | ||
|
|
75ecf1c44d | ||
|
|
5283919d24 | ||
|
|
ced8c8e497 | ||
|
|
bf7852ce85 | ||
|
|
30214fde74 | ||
|
|
e9c213f803 | ||
|
|
9f11e045a5 | ||
|
|
93ebdf724b | ||
|
|
59ce228c2e | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
1d6137f7e2 | ||
|
|
66c56e9d02 | ||
|
|
e744d4c82c | ||
|
|
0774ecc89c | ||
|
|
5e7a4c7fb5 | ||
|
|
76eaf64f94 | ||
|
|
80865b30a5 | ||
|
|
8e6248f57f | ||
|
|
268db6b1d0 | ||
|
|
479dd80a8c | ||
|
|
069431db72 | ||
|
|
bc2b310638 | ||
|
|
33bf20cfc8 | ||
|
|
e3bdc391cd | ||
|
|
5681f4dd69 | ||
|
|
38d822e04c | ||
|
|
8e274a5a84 | ||
|
|
985d2f1c2c | ||
|
|
7f5872372d | ||
|
|
201f788806 | ||
|
|
a14b5c33fd | ||
|
|
473261be35 | ||
|
|
a54be85185 | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
54e3122540 | ||
|
|
d339ab1125 | ||
|
|
3ab09ef708 | ||
|
|
c86a122d80 | ||
|
|
3a58e37838 | ||
|
|
6bd49bcd4b | ||
|
|
61577cf6bf | ||
|
|
b4dec2a99c | ||
|
|
fe0b122aca | ||
|
|
8eb2960950 | ||
|
|
c2369a740d | ||
|
|
bab6fd1f2f | ||
|
|
86fbc9a936 | ||
|
|
4d9726dbdd | ||
|
|
4442a2e6d1 | ||
|
|
293be7093c | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
354912a1df | ||
|
|
eacff3a9f0 | ||
|
|
990acbb9ac | ||
|
|
17d4533e45 | ||
|
|
d6c00a85ad | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
e0279f93f9 | ||
|
|
9b83c57316 | ||
|
|
5d73d17c74 | ||
|
|
d32460070f | ||
|
|
105500e506 | ||
|
|
8296782149 | ||
|
|
8e8d582bc6 | ||
|
|
8dec674cc3 | ||
|
|
e87db96fc0 | ||
|
|
f784c03746 | ||
|
|
4bb7f0613f | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
080acf0a62 | ||
|
|
ea2e16cabb | ||
|
|
7bcd967fd9 | ||
|
|
bb87401d10 | ||
|
|
0821672e70 | ||
|
|
14feef3679 | ||
|
|
1c8c9e65c5 | ||
|
|
14ca31768c | ||
|
|
e27a630a09 | ||
|
|
9319f99a3d | ||
|
|
d6739c1158 | ||
|
|
1d2b6fdfa2 | ||
|
|
4fc75beed4 | ||
|
|
d3709de035 | ||
|
|
7178c66cf5 | ||
|
|
f60a0c3b76 | ||
|
|
9a470b07fd | ||
|
|
0d8ca22487 | ||
|
|
3b3bc0c4bf | ||
|
|
910faab88e | ||
|
|
e519236d94 | ||
|
|
5bf811a488 | ||
|
|
3300edc51a | ||
|
|
a592eaeb91 | ||
|
|
cca3533d35 | ||
|
|
46c2e8b14e | ||
|
|
3347b3b2f5 | ||
|
|
445f939822 | ||
|
|
05c4b2089c | ||
|
|
6e75bcdc37 | ||
|
|
4db692309b |
26
.env.example
26
.env.example
@@ -7,13 +7,22 @@ APP_KEY=SomeRandomString
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=database_database
|
||||
DB_USERNAME=database_username
|
||||
DB_PASSWORD=database__user_password
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# Cache and session
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
# If using Memcached, comment the above and uncomment these
|
||||
#CACHE_DRIVER=memcached
|
||||
#SESSION_DRIVER=memcached
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# Memcached settings
|
||||
# If using a UNIX socket path for the host, set the port to 0
|
||||
# This follows the following format: HOST:PORT:WEIGHT
|
||||
# For multiple servers separate with a comma
|
||||
MEMCACHED_SERVERS=127.0.0.1:11211:100
|
||||
|
||||
# Storage
|
||||
STORAGE_TYPE=local
|
||||
# Amazon S3 Config
|
||||
@@ -25,6 +34,9 @@ STORAGE_S3_BUCKET=false
|
||||
# Used to prefix image urls for when using custom domains/cdns
|
||||
STORAGE_URL=false
|
||||
|
||||
# General auth
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Social Authentication information. Defaults as off.
|
||||
GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
@@ -33,8 +45,16 @@ GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services
|
||||
USE_GRAVATAR=true
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
# LDAP Settings
|
||||
LDAP_SERVER=false
|
||||
LDAP_BASE_DN=false
|
||||
LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
|
||||
# Mail settings
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,7 +7,6 @@ Homestead.yaml
|
||||
/public/plugins
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/uploads
|
||||
/public/bower
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
|
||||
@@ -15,15 +15,11 @@ class Activity extends Model
|
||||
|
||||
/**
|
||||
* Get the entity for this activity.
|
||||
* @return bool
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
if ($this->entity_id) {
|
||||
return $this->morphTo('entity')->first();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if ($this->entity_type === '') $this->entity_type = null;
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Entity extends Model
|
||||
abstract class Entity extends Ownable
|
||||
{
|
||||
|
||||
use Ownable;
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
@@ -31,11 +26,7 @@ abstract class Entity extends Model
|
||||
|
||||
if ($matches) return true;
|
||||
|
||||
if ($entity->isA('chapter') && $this->isA('book')) {
|
||||
return $entity->book_id === $this->id;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $this->isA('book')) {
|
||||
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
||||
return $entity->book_id === $this->id;
|
||||
}
|
||||
|
||||
@@ -57,7 +48,6 @@ abstract class Entity extends Model
|
||||
|
||||
/**
|
||||
* Get View objects for this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public function views()
|
||||
{
|
||||
@@ -65,12 +55,22 @@ abstract class Entity extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get just the views for the current user.
|
||||
* @return mixed
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
public function userViews()
|
||||
public function restrictions()
|
||||
{
|
||||
return $this->views()->where('user_id', '=', auth()->user()->id);
|
||||
return $this->morphMany('BookStack\Restriction', 'restrictable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this entity has a specific restriction set against it.
|
||||
* @param $role_id
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRestriction($role_id, $action)
|
||||
{
|
||||
return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,23 +85,14 @@ abstract class Entity extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class name.
|
||||
* @return string
|
||||
*/
|
||||
public static function getClassName()
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
*Gets a limited-length version of the entities name.
|
||||
* Gets a limited-length version of the entities name.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($length = 25)
|
||||
{
|
||||
if(strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length-3) . '...';
|
||||
if (strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,27 +102,48 @@ abstract class Entity extends Model
|
||||
* @param string[] array $wheres
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$termString = '';
|
||||
foreach ($terms as $term) {
|
||||
$termString .= $term . '* ';
|
||||
$exactTerms = [];
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/\s/', $term)) {
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
$term = '"' . $term . '"';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
}
|
||||
if ($term !== '*') $terms[$key] = $term;
|
||||
}
|
||||
$termString = implode(' ', $terms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
|
||||
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
|
||||
$search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if (!static::isA('book')) $search = $search->with('book');
|
||||
if (static::isA('page')) $search = $search->with('chapter');
|
||||
if (static::isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if (static::isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy('title_relevance', 'desc')->get();
|
||||
return $search->orderBy('title_relevance', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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 {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class ConfirmationEmailException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class ConfirmationEmailException extends NotifyException {}
|
||||
@@ -3,8 +3,12 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use PhpSpec\Exception\Example\ErrorException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@@ -14,7 +18,10 @@ class Handler extends ExceptionHandler
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
AuthorizationException::class,
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
ValidationException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -32,17 +39,27 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Exception $e)
|
||||
{
|
||||
if($e instanceof NotifyException) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
}
|
||||
|
||||
3
app/Exceptions/ImageUploadException.php
Normal file
3
app/Exceptions/ImageUploadException.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class ImageUploadException extends PrettyException {}
|
||||
3
app/Exceptions/LdapException.php
Normal file
3
app/Exceptions/LdapException.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class LdapException extends PrettyException {}
|
||||
14
app/Exceptions/NotFoundException.php
Normal file
14
app/Exceptions/NotFoundException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class NotFoundException extends PrettyException {
|
||||
|
||||
/**
|
||||
* NotFoundException constructor.
|
||||
* @param string $message
|
||||
*/
|
||||
public function __construct($message = 'Item not found')
|
||||
{
|
||||
parent::__construct($message, 404);
|
||||
}
|
||||
}
|
||||
6
app/Exceptions/PermissionsException.php
Normal file
6
app/Exceptions/PermissionsException.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class PermissionsException extends Exception {}
|
||||
5
app/Exceptions/PrettyException.php
Normal file
5
app/Exceptions/PrettyException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class SocialDriverNotConfigured extends \Exception
|
||||
{
|
||||
}
|
||||
class SocialDriverNotConfigured extends PrettyException {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class SocialSignInException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class SocialSignInException extends NotifyException {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class UserRegistrationException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class UserRegistrationException extends NotifyException {}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Exceptions\PrettyException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
@@ -29,9 +32,10 @@ class AuthController extends Controller
|
||||
|
||||
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
|
||||
|
||||
protected $loginPath = '/login';
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
@@ -39,9 +43,9 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* Create a new authentication controller instance.
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param EmailConfirmationService $emailConfirmationService
|
||||
* @param UserRepo $userRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
@@ -49,6 +53,7 @@ class AuthController extends Controller
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -60,15 +65,15 @@ class AuthController extends Controller
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'password' => 'required|min:6',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function checkRegistrationAllowed()
|
||||
{
|
||||
if (!\Setting::get('registration-enabled')) {
|
||||
if (!setting('registration-enabled')) {
|
||||
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
|
||||
}
|
||||
}
|
||||
@@ -105,6 +110,46 @@ class AuthController extends Controller
|
||||
return $this->registerUser($userData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
@@ -118,8 +163,8 @@ class AuthController extends Controller
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
@@ -127,7 +172,7 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* The registrations flow for all users.
|
||||
* @param array $userData
|
||||
* @param array $userData
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
@@ -135,8 +180,8 @@ class AuthController extends Controller
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
{
|
||||
if (\Setting::get('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict')));
|
||||
if (setting('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
|
||||
@@ -148,21 +193,19 @@ class AuthController extends Controller
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) {
|
||||
$newUser->email_confirmed = false;
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
$newUser->save();
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
$newUser->email_confirmed = true;
|
||||
auth()->login($newUser);
|
||||
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
||||
return redirect($this->redirectPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page to tell the user to check thier email
|
||||
* Show the page to tell the user to check their email
|
||||
* and confirm their address.
|
||||
*/
|
||||
public function getRegisterConfirmation()
|
||||
@@ -222,7 +265,7 @@ class AuthController extends Controller
|
||||
]);
|
||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
\Session::flash('success', 'Confirmation email resent, Please check your inbox.');
|
||||
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
@@ -232,13 +275,9 @@ class AuthController extends Controller
|
||||
*/
|
||||
public function getLogin()
|
||||
{
|
||||
|
||||
if (view()->exists('auth.authenticate')) {
|
||||
return view('auth.authenticate');
|
||||
}
|
||||
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
return view('auth.login', ['socialDrivers' => $socialDrivers]);
|
||||
$authMethod = config('auth.method');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +292,7 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the social site for authentication initended to register.
|
||||
* Redirect to the social site for authentication intended to register.
|
||||
* @param $socialDriver
|
||||
* @return mixed
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
@@ -19,24 +16,26 @@ class BookController extends Controller
|
||||
protected $bookRepo;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
@@ -50,12 +49,11 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for creating a new book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('book-create');
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->setPageTitle('Create New Book');
|
||||
return view('books/create');
|
||||
}
|
||||
@@ -68,9 +66,9 @@ class BookController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('book-create');
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->bookRepo->newFromInput($request->all());
|
||||
@@ -84,7 +82,6 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Display the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
@@ -99,31 +96,29 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($slug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->setPageTitle('Edit Book ' . $book->getShortName());
|
||||
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified book in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request, $slug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book->fill($request->all());
|
||||
@@ -141,8 +136,8 @@ class BookController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->setPageTitle('Delete Book ' . $book->getShortName());
|
||||
return view('books/delete', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
@@ -154,10 +149,10 @@ class BookController extends Controller
|
||||
*/
|
||||
public function sort($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$books = $this->bookRepo->getAll();
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->bookRepo->getChildren($book, true);
|
||||
$books = $this->bookRepo->getAll(false);
|
||||
$this->setPageTitle('Sort Book ' . $book->getShortName());
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
@@ -177,15 +172,14 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Saves an array of sort mapping to pages and chapters.
|
||||
*
|
||||
* @param string $bookSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function saveSort($bookSlug, Request $request)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
// Return if no map sent
|
||||
if (!$request->has('sort-tree')) {
|
||||
@@ -223,17 +217,48 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Remove the specified book from storage.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function destroy($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
Activity::addMessage('book_delete', 0, $book->name);
|
||||
Activity::removeEntity($book);
|
||||
$this->bookRepo->destroyBySlug($bookSlug);
|
||||
return redirect('/books');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('books/restrictions', [
|
||||
'book' => $book,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this book.
|
||||
* @param $bookSlug
|
||||
* @param $bookSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
|
||||
session()->flash('success', 'Book Restrictions Updated');
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use Views;
|
||||
@@ -17,20 +13,22 @@ class ChapterController extends Controller
|
||||
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* ChapterController constructor.
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for creating a new chapter.
|
||||
* @param $bookSlug
|
||||
@@ -38,8 +36,8 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function create($bookSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-create');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
$this->setPageTitle('Create New Chapter');
|
||||
return view('chapters/create', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
@@ -52,12 +50,13 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function store($bookSlug, Request $request)
|
||||
{
|
||||
$this->checkPermission('chapter-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$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);
|
||||
@@ -81,7 +80,14 @@ class ChapterController extends Controller
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($chapter);
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
|
||||
$pages = $this->chapterRepo->getChildren($chapter);
|
||||
return view('chapters/show', [
|
||||
'book' => $book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,9 +98,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function edit($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
|
||||
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
@@ -108,9 +114,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$chapter->fill($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
@@ -127,9 +133,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
|
||||
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
@@ -142,11 +148,46 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('chapters/restrictions', [
|
||||
'chapter' => $chapter,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $chapterSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
|
||||
session()->flash('success', 'Chapter Restrictions Updated');
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Ownable;
|
||||
use HttpRequestException;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Exception\HttpResponseException;
|
||||
@@ -42,6 +43,15 @@ abstract class Controller extends BaseController
|
||||
$this->signedIn = auth()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the application and shows a permission error if
|
||||
* the application is in demo mode.
|
||||
*/
|
||||
protected function preventAccessForDemoUsers()
|
||||
{
|
||||
if (config('app.env') === 'demo') $this->showPermissionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the page title into the view.
|
||||
* @param $title
|
||||
@@ -51,24 +61,48 @@ abstract class Controller extends BaseController
|
||||
view()->share('pageTitle', $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* On a permission error redirect to home and display.
|
||||
* the error as a notification.
|
||||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a permission.
|
||||
*
|
||||
* @param $permissionName
|
||||
* @param string $permissionName
|
||||
* @return bool|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function checkPermission($permissionName)
|
||||
{
|
||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
throw new HttpResponseException(
|
||||
redirect('/')
|
||||
);
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the current user's permissions against an ownable item.
|
||||
* @param $permission
|
||||
* @param Ownable $ownable
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkOwnablePermission($permission, Ownable $ownable)
|
||||
{
|
||||
if (userCan($permission, $ownable)) return true;
|
||||
return $this->showPermissionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a permission or bypass if the callback is true.
|
||||
* @param $permissionName
|
||||
* @param $callback
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkPermissionOr($permissionName, $callback)
|
||||
{
|
||||
$callbackResult = $callback();
|
||||
|
||||
@@ -3,39 +3,44 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use Views;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
|
||||
protected $activityService;
|
||||
protected $bookRepo;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* HomeController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the homepage.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$activity = Activity::latest();
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10);
|
||||
return view('home', ['activity' => $activity, 'recents' => $recents]);
|
||||
$activity = Activity::latest(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,
|
||||
'draftPages' => $draftPages
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +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;
|
||||
|
||||
@@ -19,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)
|
||||
@@ -31,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
|
||||
*/
|
||||
@@ -43,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
|
||||
@@ -54,22 +68,49 @@ class ImageController extends Controller
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery images with a specific filter such as book or page
|
||||
* @param $filter
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
*/
|
||||
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
$validFilters = collect(['page', 'book']);
|
||||
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||
|
||||
$pageId = $request->get('page_id');
|
||||
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles image uploads for use on pages.
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function uploadByType($type, Request $request)
|
||||
{
|
||||
$this->checkPermission('image-create');
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => 'image|mimes:jpeg,gif,png'
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type);
|
||||
|
||||
try {
|
||||
$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);
|
||||
}
|
||||
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
@@ -83,7 +124,7 @@ class ImageController extends Controller
|
||||
*/
|
||||
public function getThumbnail($id, $width, $height, $crop)
|
||||
{
|
||||
$this->checkPermission('image-create');
|
||||
$this->checkPermission('image-create-all');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
|
||||
return response()->json(['url' => $thumbnailUrl]);
|
||||
@@ -91,33 +132,32 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Update image details
|
||||
* @param $imageId
|
||||
* @param integer $imageId
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update($imageId, Request $request)
|
||||
{
|
||||
$this->checkPermission('image-update');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|min:2|string'
|
||||
]);
|
||||
$image = $this->imageRepo->getById($imageId);
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes an image and all thumbnail/image files
|
||||
* @param PageRepo $pageRepo
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy(PageRepo $pageRepo, Request $request, $id)
|
||||
{
|
||||
$this->checkPermission('image-delete');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('image-delete', $image);
|
||||
|
||||
// Check if this image is used on any pages
|
||||
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Views;
|
||||
|
||||
class PageController extends Controller
|
||||
@@ -18,24 +19,29 @@ class PageController extends Controller
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $exportService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param ExportService $exportService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->exportService = $exportService;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param bool $chapterSlug
|
||||
* @return Response
|
||||
@@ -43,35 +49,60 @@ class PageController extends Controller
|
||||
*/
|
||||
public function create($bookSlug, $chapterSlug = false)
|
||||
{
|
||||
$this->checkPermission('page-create');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
$this->setPageTitle('Create New Page');
|
||||
return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
|
||||
|
||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created page in storage.
|
||||
*
|
||||
* Show form to continue editing a draft page.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function editDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$draft = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft);
|
||||
$this->setPageTitle('Edit Page Draft');
|
||||
|
||||
return view('pages/create', ['draft' => $draft, 'book' => $book]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new page by changing a draft into a page.
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param string $bookSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request, $bookSlug)
|
||||
public function store(Request $request, $bookSlug, $pageId)
|
||||
{
|
||||
$this->checkPermission('page-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'html' => 'required|string',
|
||||
'parent' => 'integer|exists:pages,id'
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$input = $request->all();
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($book);
|
||||
|
||||
$page = $this->pageRepo->saveNew($input, $book, $chapterId);
|
||||
$draftPage = $this->pageRepo->getById($pageId, true);
|
||||
|
||||
$chapterId = $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());
|
||||
@@ -79,7 +110,8 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Display the specified page.
|
||||
*
|
||||
* If the page is not found via the slug the
|
||||
* revisions are searched for a match.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return Response
|
||||
@@ -87,32 +119,69 @@ class PageController extends Controller
|
||||
public function show($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
|
||||
try {
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page from an ajax request.
|
||||
* @param $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getPageAjax($pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
return response()->json($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle('Editing Page ' . $page->getShortName());
|
||||
$page->isDraft = false;
|
||||
|
||||
// Check for active editing
|
||||
$warnings = [];
|
||||
if ($this->pageRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
$page->name = $draft->name;
|
||||
$page->html = $draft->html;
|
||||
$page->markdown = $draft->markdown;
|
||||
$page->isDraft = true;
|
||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||
|
||||
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified page in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
@@ -120,14 +189,42 @@ class PageController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->pageRepo->updatePage($page, $book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a draft update as a revision.
|
||||
* @param Request $request
|
||||
* @param $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function saveDraft(Request $request, $pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
if ($page->draft) {
|
||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||
} else {
|
||||
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
}
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Draft saved at ',
|
||||
'timestamp' => $utcUpdateTimestamp
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect from a special link url which
|
||||
* uses the page id rather than the name.
|
||||
@@ -148,16 +245,32 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->setPageTitle('Delete Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the deletion page for the specified page.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @return \Illuminate\View\View
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showDeleteDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle('Delete Draft Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified page from storage.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return Response
|
||||
@@ -165,10 +278,28 @@ class PageController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||
session()->flash('success', 'Page deleted');
|
||||
$this->pageRepo->destroy($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified draft page from storage.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function destroyDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
session()->flash('success', 'Draft deleted');
|
||||
$this->pageRepo->destroy($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -213,11 +344,125 @@ class PageController extends Controller
|
||||
*/
|
||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
|
||||
* https://github.com/barryvdh/laravel-dompdf
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPdf($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$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'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a page to a self-contained HTML file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportHtml($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$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'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a page to a simple plaintext .txt file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$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'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of recently created pages
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRecentlyCreated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Created Pages',
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of recently created pages
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Updated Pages',
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('pages/restrictions', [
|
||||
'page' => $page,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->pageRepo->updateRestrictionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Restrictions Updated');
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
129
app/Http/Controllers/PermissionController.php
Normal file
129
app/Http/Controllers/PermissionController.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
|
||||
protected $permissionsRepo;
|
||||
|
||||
/**
|
||||
* PermissionController constructor.
|
||||
* @param PermissionsRepo $permissionsRepo
|
||||
*/
|
||||
public function __construct(PermissionsRepo $permissionsRepo)
|
||||
{
|
||||
$this->permissionsRepo = $permissionsRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of the roles in the system.
|
||||
*/
|
||||
public function listRoles()
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$roles = $this->permissionsRepo->getAllRoles();
|
||||
return view('settings/roles/index', ['roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form to create a new role
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function createRole()
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
return view('settings/roles/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new role in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function storeRole(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->saveNewRole($request->all());
|
||||
session()->flash('success', 'Role successfully created');
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing a user role.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function editRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
return view('settings/roles/edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a user role.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function updateRole($id, Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->updateRole($id, $request->all());
|
||||
session()->flash('success', 'Role successfully updated');
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view to delete a role.
|
||||
* Offers the chance to migrate users.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showDeleteRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
|
||||
$roles->prepend($blankRole);
|
||||
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the system,
|
||||
* Migrate from a previous role if set.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function deleteRole($id, Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
|
||||
try {
|
||||
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
|
||||
} catch (PermissionsException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Role successfully deleted');
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,77 @@ class SearchController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm);
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle('Search For ' . $searchTerm);
|
||||
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the pages in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchPages(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$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',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the chapters in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchChapters(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$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',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the books in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchBooks(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$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',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,9 +128,9 @@ class SearchController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$whereTerm = [['book_id', '=', $bookId]];
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, $whereTerm);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, $whereTerm);
|
||||
$searchWhereTerms = [['book_id', '=', $bookId]];
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Setting;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the settings.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->checkPermission('settings-update');
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->setPageTitle('Settings');
|
||||
return view('settings/index');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified settings in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$this->checkPermission('settings-update');
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermission('settings-manage');
|
||||
|
||||
// Cycles through posted settings and update them
|
||||
foreach($request->all() as $name => $value) {
|
||||
if(strpos($name, 'setting-') !== 0) continue;
|
||||
foreach ($request->all() as $name => $value) {
|
||||
if (strpos($name, 'setting-') !== 0) continue;
|
||||
$key = str_replace('setting-', '', trim($name));
|
||||
Setting::put($key, $value);
|
||||
}
|
||||
|
||||
session()->flash('success', 'Settings Saved');
|
||||
return redirect('/settings');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
@@ -34,7 +35,8 @@ class UserController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$users = $this->user->all();
|
||||
$this->checkPermission('users-manage');
|
||||
$users = $this->userRepo->getAllUsers();
|
||||
$this->setPageTitle('Users');
|
||||
return view('users/index', ['users' => $users]);
|
||||
}
|
||||
@@ -45,8 +47,9 @@ class UserController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
return view('users/create');
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
return view('users/create', ['authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,32 +59,47 @@ class UserController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
$this->validate($request, [
|
||||
$this->checkPermission('users-manage');
|
||||
$validationRules = [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|min:5',
|
||||
'password-confirm' => 'required|same:password',
|
||||
'role' => 'required|exists:roles,id'
|
||||
]);
|
||||
'email' => 'required|email|unique:users,email'
|
||||
];
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
if ($authMethod === 'standard') {
|
||||
$validationRules['password'] = 'required|min:5';
|
||||
$validationRules['password-confirm'] = 'required|same:password';
|
||||
} elseif ($authMethod === 'ldap') {
|
||||
$validationRules['external_auth_id'] = 'required';
|
||||
}
|
||||
$this->validate($request, $validationRules);
|
||||
|
||||
|
||||
$user = $this->user->fill($request->all());
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
|
||||
if ($authMethod === 'standard') {
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
} elseif ($authMethod === 'ldap') {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
$user->attachRoleId($request->get('role'));
|
||||
if ($request->has('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!env('DISABLE_EXTERNAL_SERVICES', false)) {
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return redirect('/users');
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified user.
|
||||
* @param int $id
|
||||
@@ -90,14 +108,16 @@ class UserController extends Controller
|
||||
*/
|
||||
public function edit($id, SocialAuthService $socialAuthService)
|
||||
{
|
||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]);
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,30 +128,45 @@ class UserController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5',
|
||||
'password-confirm' => 'same:password',
|
||||
'role' => 'exists:roles,id'
|
||||
'name' => 'min:2',
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password'
|
||||
], [
|
||||
'password-confirm.required_with' => 'Password confirmation required'
|
||||
]);
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$user->fill($request->except('password'));
|
||||
$user->fill($request->all());
|
||||
|
||||
if ($this->currentUser->can('user-update') && $request->has('role')) {
|
||||
$user->attachRoleId($request->get('role'));
|
||||
// Role updates
|
||||
if (userCan('users-manage') && $request->has('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Password updates
|
||||
if ($request->has('password') && $request->get('password') != '') {
|
||||
$password = $request->get('password');
|
||||
$user->password = bcrypt($password);
|
||||
}
|
||||
|
||||
// External auth id updates
|
||||
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return redirect('/users');
|
||||
session()->flash('success', 'User successfully updated');
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||
return redirect($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,9 +176,10 @@ class UserController extends Controller
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$this->setPageTitle('Delete User ' . $user->name);
|
||||
return view('users/delete', ['user' => $user]);
|
||||
@@ -156,7 +192,8 @@ class UserController extends Controller
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
@@ -167,6 +204,25 @@ class UserController extends Controller
|
||||
}
|
||||
$this->userRepo->destroy($user);
|
||||
|
||||
return redirect('/users');
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showProfilePage($id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
$userActivity = $this->userRepo->getActivity($user);
|
||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||
return view('users/profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 +26,17 @@ 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) {
|
||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||
return redirect()->guest('/register/confirm/awaiting');
|
||||
}
|
||||
if ($this->auth->guest() && !Setting::get('app-public')) {
|
||||
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
// Authenticated routes...
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
Route::group(['prefix' => 'pages'], function() {
|
||||
Route::get('/recently-created', 'PageController@showRecentlyCreated');
|
||||
Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'books'], function () {
|
||||
|
||||
// Books
|
||||
@@ -14,21 +19,30 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::delete('/{id}', 'BookController@destroy');
|
||||
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
|
||||
Route::get('/{slug}', 'BookController@show');
|
||||
Route::get('/{bookSlug}/permissions', 'BookController@showRestrict');
|
||||
Route::put('/{bookSlug}/permissions', 'BookController@restrict');
|
||||
Route::get('/{slug}/delete', 'BookController@showDelete');
|
||||
Route::get('/{bookSlug}/sort', 'BookController@sort');
|
||||
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
|
||||
|
||||
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
Route::post('/{bookSlug}/page', 'PageController@store');
|
||||
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
|
||||
Route::post('/{bookSlug}/page/{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}/delete', 'PageController@showDelete');
|
||||
Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@restrict');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
|
||||
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
|
||||
Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
|
||||
|
||||
//Revisions
|
||||
// Revisions
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
|
||||
@@ -40,20 +54,15 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
|
||||
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
Route::get('/users/{id}/delete', 'UserController@delete');
|
||||
Route::post('/users/create', 'UserController@store');
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
Route::delete('/users/{id}', 'UserController@destroy');
|
||||
// User Profile routes
|
||||
Route::get('/user/{userId}', 'UserController@showProfilePage');
|
||||
|
||||
// Image routes
|
||||
Route::group(['prefix' => 'images'], function() {
|
||||
@@ -66,14 +75,24 @@ 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');
|
||||
|
||||
// Links
|
||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||
|
||||
// Search
|
||||
Route::get('/search/all', 'SearchController@searchAll');
|
||||
Route::get('/search/pages', 'SearchController@searchPages');
|
||||
Route::get('/search/books', 'SearchController@searchBooks');
|
||||
Route::get('/search/chapters', 'SearchController@searchChapters');
|
||||
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
|
||||
|
||||
// Other Pages
|
||||
@@ -81,8 +100,28 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/home', 'HomeController@index');
|
||||
|
||||
// Settings
|
||||
Route::get('/settings', 'SettingController@index');
|
||||
Route::post('/settings', 'SettingController@update');
|
||||
Route::group(['prefix' => 'settings'], function() {
|
||||
Route::get('/', 'SettingController@index');
|
||||
Route::post('/', 'SettingController@update');
|
||||
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
Route::get('/users/{id}/delete', 'UserController@delete');
|
||||
Route::post('/users/create', 'UserController@store');
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
Route::delete('/users/{id}', 'UserController@destroy');
|
||||
|
||||
// Roles
|
||||
Route::get('/roles', 'PermissionController@listRoles');
|
||||
Route::get('/roles/new', 'PermissionController@createRole');
|
||||
Route::post('/roles/new', 'PermissionController@storeRole');
|
||||
Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
|
||||
Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
|
||||
Route::get('/roles/{id}', 'PermissionController@editRole');
|
||||
Route::put('/roles/{id}', 'PermissionController@updateRole');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Images;
|
||||
|
||||
class Image extends Model
|
||||
class Image extends Ownable
|
||||
{
|
||||
use Ownable;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
trait Ownable
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
/**
|
||||
* Relation for the user that created this entity.
|
||||
@@ -20,4 +21,14 @@ trait Ownable
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class name.
|
||||
* @return string
|
||||
*/
|
||||
public static function getClassName()
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
}
|
||||
|
||||
}
|
||||
11
app/Page.php
11
app/Page.php
@@ -6,7 +6,7 @@ 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'];
|
||||
|
||||
@@ -34,18 +34,21 @@ class Page extends Entity
|
||||
|
||||
public function revisions()
|
||||
{
|
||||
return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc');
|
||||
return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
$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;
|
||||
return '/books/' . $bookSlug . $midText . $idComponent;
|
||||
}
|
||||
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text'];
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this revision originates from.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this revision.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
|
||||
@@ -11,6 +11,16 @@ class Permission extends Model
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permissions');
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
{
|
||||
return static::where('name', '=', $name)->first();
|
||||
}
|
||||
}
|
||||
|
||||
31
app/Providers/AuthServiceProvider.php
Normal file
31
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,17 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
return new ActivityService($this->app->make('BookStack\Activity'));
|
||||
return new ActivityService(
|
||||
$this->app->make('BookStack\Activity'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
return new ViewService($this->app->make('BookStack\View'));
|
||||
return new ViewService(
|
||||
$this->app->make('BookStack\View'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('setting', function() {
|
||||
@@ -41,6 +47,7 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('images', function() {
|
||||
return new ImageService(
|
||||
$this->app->make('Intervention\Image\ImageManager'),
|
||||
|
||||
133
app/Providers/LdapUserProvider.php
Normal file
133
app/Providers/LdapUserProvider.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\LdapService;
|
||||
use BookStack\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
|
||||
class LdapUserProvider implements UserProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* The user model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var LdapService
|
||||
*/
|
||||
protected $ldapService;
|
||||
|
||||
|
||||
/**
|
||||
* LdapUserProvider constructor.
|
||||
* @param $model
|
||||
* @param LdapService $ldapService
|
||||
*/
|
||||
public function __construct($model, LdapService $ldapService)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->ldapService = $ldapService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function createModel()
|
||||
{
|
||||
$class = '\\' . ltrim($this->model, '\\');
|
||||
return new $class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a user by their unique identifier.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveById($identifier)
|
||||
{
|
||||
return $this->createModel()->newQuery()->find($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by their unique identifier and "remember me" token.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @param string $token
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
{
|
||||
$model = $this->createModel();
|
||||
|
||||
return $model->newQuery()
|
||||
->where($model->getAuthIdentifierName(), $identifier)
|
||||
->where($model->getRememberTokenName(), $token)
|
||||
->first();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the "remember me" token for the given user in storage.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
{
|
||||
if ($user->exists) {
|
||||
$user->setRememberToken($token);
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
// Get user via LDAP
|
||||
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
||||
if ($userDetails === null) return null;
|
||||
|
||||
// Search current user base by looking up a uid
|
||||
$model = $this->createModel();
|
||||
$currentUser = $model->newQuery()
|
||||
->where('external_auth_id', $userDetails['uid'])
|
||||
->first();
|
||||
|
||||
if ($currentUser !== null) return $currentUser;
|
||||
|
||||
$model->name = $userDetails['name'];
|
||||
$model->external_auth_id = $userDetails['uid'];
|
||||
$model->email = $userDetails['email'];
|
||||
$model->email_confirmed = false;
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a user against the given credentials.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
{
|
||||
return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,35 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
use Views;
|
||||
|
||||
class BookRepo
|
||||
class BookRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $book;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* BookRepo constructor.
|
||||
* @param Book $book
|
||||
* @param PageRepo $pageRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
*/
|
||||
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
{
|
||||
$this->book = $book;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting books.
|
||||
* Takes into account any restrictions.
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +39,7 @@ class BookRepo
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->book->findOrFail($id);
|
||||
return $this->bookQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +49,9 @@ class BookRepo
|
||||
*/
|
||||
public function getAll($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('name', 'asc')->take($count)->get();
|
||||
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
|
||||
if (!$count) return $bookQuery->get();
|
||||
return $bookQuery->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +61,8 @@ class BookRepo
|
||||
*/
|
||||
public function getAllPaginated($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('name', 'asc')->paginate($count);
|
||||
return $this->bookQuery()
|
||||
->orderBy('name', 'asc')->paginate($count);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +73,7 @@ class BookRepo
|
||||
*/
|
||||
public function getLatest($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('created_at', 'desc')->take($count)->get();
|
||||
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +102,13 @@ class BookRepo
|
||||
* Get a book by slug
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug)
|
||||
{
|
||||
return $this->book->where('slug', '=', $slug)->first();
|
||||
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
|
||||
if ($book === null) throw new NotFoundException('Book not found');
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +118,7 @@ class BookRepo
|
||||
*/
|
||||
public function exists($id)
|
||||
{
|
||||
return $this->book->where('id', '=', $id)->exists();
|
||||
return $this->bookQuery()->where('id', '=', $id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,17 +128,7 @@ class BookRepo
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
return $this->book->fill($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the amount of books that have a specific slug.
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
*/
|
||||
public function countBySlug($slug)
|
||||
{
|
||||
return $this->book->where('slug', '=', $slug)->count();
|
||||
return $this->book->newInstance($input);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,6 +145,7 @@ class BookRepo
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->restrictions()->delete();
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
@@ -157,7 +161,7 @@ class BookRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
* @param string $slug
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
@@ -173,7 +177,7 @@ class BookRepo
|
||||
/**
|
||||
* Provides a suitable slug for the given book name.
|
||||
* Ensures the returned slug is unique in the system.
|
||||
* @param string $name
|
||||
* @param string $name
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
@@ -194,35 +198,64 @@ class BookRepo
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
|
||||
* @param Book $book
|
||||
* @param bool $filterDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChildren(Book $book)
|
||||
public function getChildren(Book $book, $filterDrafts = false)
|
||||
{
|
||||
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
|
||||
$chapters = $book->chapters()->with('pages')->get();
|
||||
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
|
||||
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
|
||||
if ($filterDrafts) {
|
||||
$pageQuery = $pageQuery->where('draft', '=', false);
|
||||
}
|
||||
|
||||
$pages = $pageQuery->get();
|
||||
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) {
|
||||
$this->restrictionService->enforcePageRestrictions($query, 'view');
|
||||
if ($filterDrafts) $query->where('draft', '=', false);
|
||||
}]);
|
||||
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapters = $chapterQuery->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get books by search term.
|
||||
* @param $term
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term)
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', preg_quote(trim($term)));
|
||||
$books = $this->book->fullTextSearch(['name', 'description'], $terms);
|
||||
$words = join('|', $terms);
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
|
||||
|
||||
@@ -2,21 +2,19 @@
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
||||
class ChapterRepo
|
||||
class ChapterRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $chapter;
|
||||
|
||||
/**
|
||||
* ChapterRepo constructor.
|
||||
* @param $chapter
|
||||
* Base query for getting chapters, Takes restrictions into account.
|
||||
* @return mixed
|
||||
*/
|
||||
public function __construct(Chapter $chapter)
|
||||
private function chapterQuery()
|
||||
{
|
||||
$this->chapter = $chapter;
|
||||
return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +24,7 @@ class ChapterRepo
|
||||
*/
|
||||
public function idExists($id)
|
||||
{
|
||||
return $this->chapter->where('id', '=', $id)->count() > 0;
|
||||
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +34,7 @@ class ChapterRepo
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->chapter->findOrFail($id);
|
||||
return $this->chapterQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +43,7 @@ class ChapterRepo
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->chapter->all();
|
||||
return $this->chapterQuery()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,10 +51,28 @@ class ChapterRepo
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
return $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($chapter === null) throw new NotFoundException('Chapter not found');
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter
|
||||
* @param Chapter $chapter
|
||||
*/
|
||||
public function getChildren(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->restrictionService->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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +99,7 @@ class ChapterRepo
|
||||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->restrictions()->delete();
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
@@ -119,17 +136,32 @@ class ChapterRepo
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new priority value for a new page to be added
|
||||
* to the given chapter.
|
||||
* @param Chapter $chapter
|
||||
* @return int
|
||||
*/
|
||||
public function getNewPriority(Chapter $chapter)
|
||||
{
|
||||
$lastPage = $chapter->pages->last();
|
||||
return $lastPage !== null ? $lastPage->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chapters by the given search term.
|
||||
* @param $term
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', preg_quote(trim($term)));
|
||||
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms);
|
||||
$words = join('|', $terms);
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
|
||||
|
||||
177
app/Repos/EntityRepo.php
Normal file
177
app/Repos/EntityRepo.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use BookStack\User;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Book $book
|
||||
*/
|
||||
public $book;
|
||||
|
||||
/**
|
||||
* @var Chapter
|
||||
*/
|
||||
public $chapter;
|
||||
|
||||
/**
|
||||
* @var Page
|
||||
*/
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var RestrictionService
|
||||
*/
|
||||
protected $restrictionService;
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->book = app(Book::class);
|
||||
$this->chapter = app(Chapter::class);
|
||||
$this->page = app(Page::class);
|
||||
$this->restrictionService = app(RestrictionService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest books added to the system.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool $additionalQuery
|
||||
* @return
|
||||
*/
|
||||
public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently updated books.
|
||||
* @param $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book)
|
||||
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool $additionalQuery
|
||||
* @return
|
||||
*/
|
||||
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforcePageRestrictions($this->page)
|
||||
->orderBy('created_at', 'desc')->where('draft', '=', false);
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->with('book')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest chapters added to the system.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool $additionalQuery
|
||||
* @return
|
||||
*/
|
||||
public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforceChapterRestrictions($this->chapter)
|
||||
->orderBy('created_at', 'desc');
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently updated pages.
|
||||
* @param $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->restrictionService->enforcePageRestrictions($this->page)
|
||||
->where('draft', '=', false)
|
||||
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft pages owned by the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
*/
|
||||
public function getUserDraftPages($count = 20, $page = 0)
|
||||
{
|
||||
$user = auth()->user();
|
||||
return $this->page->where('draft', '=', true)
|
||||
->where('created_by', '=', $user->id)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity restrictions from a request
|
||||
* @param $request
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function updateRestrictionsFromRequest($request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
|
||||
$entity->restrictions()->delete();
|
||||
if ($request->has('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->restrictions()->create([
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a string of search terms by turning
|
||||
* it into an array of terms.
|
||||
* Keeps quoted terms together.
|
||||
* @param $termString
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareSearchTerms($termString)
|
||||
{
|
||||
preg_match_all('/"(.*?)"/', $termString, $matches);
|
||||
if (count($matches[1]) > 0) {
|
||||
$terms = $matches[1];
|
||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||
} else {
|
||||
$terms = [];
|
||||
}
|
||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
||||
return $terms;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
@@ -11,16 +13,22 @@ class ImageRepo
|
||||
|
||||
protected $image;
|
||||
protected $imageService;
|
||||
protected $restictionService;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
* @param Image $image
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService)
|
||||
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
$this->restictionService = $restrictionService;
|
||||
$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->restictionService->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,59 +1,53 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
|
||||
class PageRepo
|
||||
class PageRepo extends EntityRepo
|
||||
{
|
||||
protected $page;
|
||||
|
||||
protected $pageRevision;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
* @param Page $page
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(Page $page, PageRevision $pageRevision)
|
||||
public function __construct(PageRevision $pageRevision)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page id exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
* Base query for getting pages, Takes restrictions into account.
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function idExists($id)
|
||||
private function pageQuery($allowDrafts = false)
|
||||
{
|
||||
return $this->page->where('page_id', '=', $id)->count() > 0;
|
||||
$query = $this->restrictionService->enforcePageRestrictions($this->page, 'view');
|
||||
if (!$allowDrafts) {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page via a specific ID.
|
||||
* @param $id
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
public function getById($id, $allowDrafts = false)
|
||||
{
|
||||
return $this->page->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->page->all();
|
||||
return $this->pageQuery($allowDrafts)->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,13 +55,37 @@ class PageRepo
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($page === null) throw new NotFoundException('Page not found');
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through page revisions and retrieve
|
||||
* the last page in the current book that
|
||||
* has a slug equal to the one given.
|
||||
* @param $pageSlug
|
||||
* @param $bookSlug
|
||||
* @return null | Page
|
||||
*/
|
||||
public function findPageUsingOldSlug($pageSlug, $bookSlug)
|
||||
{
|
||||
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function ($query) {
|
||||
$this->restrictionService->enforcePageRestrictions($query);
|
||||
})
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
|
||||
->with('page')->first();
|
||||
return $revision !== null ? $revision->page : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new Page instance from the given input.
|
||||
* @param $input
|
||||
* @return Page
|
||||
*/
|
||||
@@ -92,8 +110,8 @@ class PageRepo
|
||||
* Save a new page into the system.
|
||||
* Input validation must be done beforehand.
|
||||
* @param array $input
|
||||
* @param Book $book
|
||||
* @param int $chapterId
|
||||
* @param Book $book
|
||||
* @param int $chapterId
|
||||
* @return Page
|
||||
*/
|
||||
public function saveNew(array $input, Book $book, $chapterId = null)
|
||||
@@ -112,6 +130,47 @@ class PageRepo
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Publish a draft page to make it a normal page.
|
||||
* Sets the slug and updates the content.
|
||||
* @param Page $draftPage
|
||||
* @param array $input
|
||||
* @return Page
|
||||
*/
|
||||
public function publishDraft(Page $draftPage, array $input)
|
||||
{
|
||||
$draftPage->fill($input);
|
||||
|
||||
$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();
|
||||
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);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly
|
||||
* within the system.
|
||||
@@ -120,23 +179,23 @@ class PageRepo
|
||||
*/
|
||||
protected function formatHtml($htmlText)
|
||||
{
|
||||
if ($htmlText == '') return $htmlText;
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadHTML($htmlText);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
|
||||
// Ensure no duplicate ids are used
|
||||
$lastId = false;
|
||||
$idArray = [];
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') continue;
|
||||
|
||||
// Overwrite id if not a bookstack custom id
|
||||
// Overwrite id if not a BookStack custom id
|
||||
if ($childNode->hasAttribute('id')) {
|
||||
$id = $childNode->getAttribute('id');
|
||||
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
|
||||
@@ -146,13 +205,18 @@ class PageRepo
|
||||
}
|
||||
|
||||
// Create an unique id for the element
|
||||
do {
|
||||
$id = 'bkmrk-' . substr(uniqid(), -5);
|
||||
} while ($id == $lastId);
|
||||
$lastId = $id;
|
||||
// Uses the content as a basis to ensure output is the same every time
|
||||
// the same content is passed through.
|
||||
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
|
||||
$newId = urlencode($contentId);
|
||||
$loopIndex = 0;
|
||||
while (in_array($newId, $idArray)) {
|
||||
$newId = urlencode($contentId . '-' . $loopIndex);
|
||||
$loopIndex++;
|
||||
}
|
||||
|
||||
$childNode->setAttribute('id', $id);
|
||||
$idArray[] = $id;
|
||||
$childNode->setAttribute('id', $newId);
|
||||
$idArray[] = $newId;
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
@@ -168,17 +232,20 @@ class PageRepo
|
||||
/**
|
||||
* Gets pages by a search term.
|
||||
* Highlights page content for showing in results.
|
||||
* @param string $term
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', preg_quote(trim($term)));
|
||||
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
|
||||
// Add highlights to page text.
|
||||
$words = join('|', $terms);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
//lookahead/behind assertions ensures cut between words
|
||||
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
|
||||
|
||||
@@ -212,7 +279,7 @@ class PageRepo
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
@@ -223,8 +290,8 @@ class PageRepo
|
||||
|
||||
/**
|
||||
* Updates a page with any fillable data and saves it into the database.
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param string $input
|
||||
* @return Page
|
||||
*/
|
||||
@@ -235,13 +302,23 @@ class PageRepo
|
||||
$this->saveRevision($page);
|
||||
}
|
||||
|
||||
// Prevent slug being updated if no name change
|
||||
if ($page->name !== $input['name']) {
|
||||
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$userId = auth()->user()->id;
|
||||
$page->fill($input);
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
|
||||
$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();
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -272,9 +349,13 @@ class PageRepo
|
||||
public function saveRevision(Page $page)
|
||||
{
|
||||
$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->save();
|
||||
// Clear old revisions
|
||||
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
||||
@@ -284,6 +365,155 @@ class PageRepo
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a page update draft.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return PageRevision
|
||||
*/
|
||||
public function saveUpdateDraft(Page $page, $data = [])
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
|
||||
|
||||
if ($drafts->count() > 0) {
|
||||
$draft = $drafts->first();
|
||||
} else {
|
||||
$draft = $this->pageRevision->newInstance();
|
||||
$draft->page_id = $page->id;
|
||||
$draft->slug = $page->slug;
|
||||
$draft->book_slug = $page->book->slug;
|
||||
$draft->created_by = $userId;
|
||||
$draft->type = 'update_draft';
|
||||
}
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a draft page.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return Page
|
||||
*/
|
||||
public function updateDraftPage(Page $page, $data = [])
|
||||
{
|
||||
$page->fill($data);
|
||||
|
||||
if (isset($data['html'])) {
|
||||
$page->text = strip_tags($data['html']);
|
||||
}
|
||||
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for getting user update drafts.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
private function userUpdateDraftsQuery(Page $page, $userId)
|
||||
{
|
||||
return $this->pageRevision->where('created_by', '=', $userId)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user has a draft version of a particular page or not.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUserGotPageDraft(Page $page, $userId)
|
||||
{
|
||||
return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest updated draft revision for a particular page and user.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserPageDraft(Page $page, $userId)
|
||||
{
|
||||
return $this->userUpdateDraftsQuery($page, $userId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification message that informs the user that they are editing a draft page.
|
||||
* @param PageRevision $draft
|
||||
* @return string
|
||||
*/
|
||||
public function getUserPageDraftMessage(PageRevision $draft)
|
||||
{
|
||||
$message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
|
||||
if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
|
||||
$message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is being actively editing.
|
||||
* Checks for edits since last page updated.
|
||||
* Passing in a minuted range will check for edits
|
||||
* within the last x minutes.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return bool
|
||||
*/
|
||||
public function isPageEditingActive(Page $page, $minRange = null)
|
||||
{
|
||||
$draftSearch = $this->activePageEditingQuery($page, $minRange);
|
||||
return $draftSearch->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notification message concerning the editing activity on
|
||||
* a particular page.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return string
|
||||
*/
|
||||
public function getPageEditingActiveMessage(Page $page, $minRange = null)
|
||||
{
|
||||
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
|
||||
$userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
|
||||
$timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
|
||||
$message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
|
||||
return sprintf($message, $userMessage, $timeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* A query to check for active update drafts on a particular page.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return mixed
|
||||
*/
|
||||
private function activePageEditingQuery(Page $page, $minRange = null)
|
||||
{
|
||||
$query = $this->pageRevision->where('type', '=', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->where('updated_at', '>', $page->updated_at)
|
||||
->where('created_by', '!=', auth()->user()->id)
|
||||
->with('createdBy');
|
||||
|
||||
if ($minRange !== null) {
|
||||
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single revision via it's id.
|
||||
* @param $id
|
||||
@@ -311,7 +541,7 @@ class PageRepo
|
||||
/**
|
||||
* Changes the related book for the specified page.
|
||||
* Changes the book id of any relations to the page that store the book id.
|
||||
* @param int $bookId
|
||||
* @param int $bookId
|
||||
* @param Page $page
|
||||
* @return Page
|
||||
*/
|
||||
@@ -352,8 +582,26 @@ class PageRepo
|
||||
Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->restrictions()->delete();
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($count = 20)
|
||||
{
|
||||
return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($count = 20)
|
||||
{
|
||||
return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
142
app/Repos/PermissionsRepo.php
Normal file
142
app/Repos/PermissionsRepo.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Permission;
|
||||
use BookStack\Role;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param $permission
|
||||
* @param $role
|
||||
*/
|
||||
public function __construct(Permission $permission, Role $role)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the user roles from the system.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles except for the provided one.
|
||||
* @param Role $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role)
|
||||
{
|
||||
return $this->role->where('id', '!=', $role->id)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role via its ID.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRoleById($id)
|
||||
{
|
||||
return $this->role->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new role into the system.
|
||||
* @param array $roleData
|
||||
* @return Role
|
||||
*/
|
||||
public function saveNewRole($roleData)
|
||||
{
|
||||
$role = $this->role->newInstance($roleData);
|
||||
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
|
||||
// Prevent duplicate names
|
||||
while ($this->role->where('name', '=', $role->name)->count() > 0) {
|
||||
$role->name .= strtolower(str_random(2));
|
||||
}
|
||||
$role->save();
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing role.
|
||||
* Ensure Admin role always has all permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
*/
|
||||
public function updateRole($roleId, $roleData)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
if ($role->name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign an list of permission names to an role.
|
||||
* @param Role $role
|
||||
* @param array $permissionNameArray
|
||||
*/
|
||||
public function assignRolePermissions(Role $role, $permissionNameArray = [])
|
||||
{
|
||||
$permissions = [];
|
||||
$permissionNameArray = array_values($permissionNameArray);
|
||||
if ($permissionNameArray && count($permissionNameArray) > 0) {
|
||||
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
|
||||
}
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the system.
|
||||
* Check it's not an admin role or set as default before deleting.
|
||||
* If an migration Role ID is specified the users assign to the current role
|
||||
* will be added to the role of the specified id.
|
||||
* @param $roleId
|
||||
* @param $migrateRoleId
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function deleteRole($roleId, $migrateRoleId)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->name === 'admin') {
|
||||
throw new PermissionsException('The admin role cannot be deleted');
|
||||
} else if ($role->id == setting('registration-role')) {
|
||||
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
|
||||
}
|
||||
|
||||
if ($migrateRoleId) {
|
||||
$newRole = $this->role->find($migrateRoleId);
|
||||
if ($newRole) {
|
||||
$users = $role->users->pluck('id')->toArray();
|
||||
$newRole->users()->sync($users);
|
||||
}
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Setting;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
|
||||
protected $user;
|
||||
protected $role;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* UserRepo constructor.
|
||||
* @param $user
|
||||
* @param User $user
|
||||
* @param Role $role
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(User $user, Role $role)
|
||||
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->role = $role;
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,6 +42,15 @@ class UserRepo
|
||||
return $this->user->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsers()
|
||||
{
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
@@ -47,6 +60,14 @@ class UserRepo
|
||||
{
|
||||
$user = $this->create($data);
|
||||
$this->attachDefaultRole($user);
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -56,8 +77,8 @@ class UserRepo
|
||||
*/
|
||||
public function attachDefaultRole($user)
|
||||
{
|
||||
$roleId = \Setting::get('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->getDefault()->id;
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->first()->id;
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
|
||||
@@ -68,15 +89,10 @@ class UserRepo
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
{
|
||||
if ($user->role->name != 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminRole = $this->role->where('name', '=', 'admin')->first();
|
||||
if (count($adminRole->users) > 1) {
|
||||
return false;
|
||||
}
|
||||
if (!$user->roles->pluck('name')->contains('admin')) return false;
|
||||
|
||||
$adminRole = $this->role->getRole('admin');
|
||||
if ($adminRole->users->count() > 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -87,10 +103,11 @@ class UserRepo
|
||||
*/
|
||||
public function create(array $data)
|
||||
{
|
||||
return $this->user->create([
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password'])
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => false
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -103,4 +120,62 @@ class UserRepo
|
||||
$user->socialAccounts()->delete();
|
||||
$user->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest activity for a user.
|
||||
* @param User $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function getActivity(User $user, $count = 20, $page = 0)
|
||||
{
|
||||
return \Activity::userActivity($user, $count, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recently created content for this given user.
|
||||
* @param User $user
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreated(User $user, $count = 20)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset created counts for the give user.
|
||||
* @param User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetCounts(User $user)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles which can be given restricted access to
|
||||
* other entities in the system.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('name', '!=', 'admin')->get();
|
||||
}
|
||||
|
||||
}
|
||||
21
app/Restriction.php
Normal file
21
app/Restriction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Restriction extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['role_id', 'action'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get all this restriction's attached entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function restrictable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
34
app/Role.php
34
app/Role.php
@@ -6,11 +6,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
/**
|
||||
* Sets the default role name for newly registed users.
|
||||
* @var string
|
||||
*/
|
||||
protected static $default = 'viewer';
|
||||
|
||||
protected $fillable = ['display_name', 'description'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
@@ -28,6 +25,15 @@ class Role extends Model
|
||||
return $this->belongsToMany('BookStack\Permission');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role has a permission.
|
||||
* @param $permission
|
||||
*/
|
||||
public function hasPermission($permission)
|
||||
{
|
||||
return $this->permissions->pluck('name')->contains($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param Permission $permission
|
||||
@@ -38,11 +44,21 @@ class Role extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the default role.
|
||||
* @return Role
|
||||
* Detach a single permission from this role.
|
||||
* @param Permission $permission
|
||||
*/
|
||||
public static function getDefault()
|
||||
public function detachPermission(Permission $permission)
|
||||
{
|
||||
return static::where('name', '=', static::$default)->first();
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified role.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getRole($roleName)
|
||||
{
|
||||
return static::where('name', '=', $roleName)->first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Activity;
|
||||
use BookStack\Entity;
|
||||
use Session;
|
||||
@@ -9,14 +8,17 @@ class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param $activity
|
||||
* @param Activity $activity
|
||||
* @param RestrictionService $restrictionService
|
||||
*/
|
||||
public function __construct(Activity $activity)
|
||||
public function __construct(Activity $activity, RestrictionService $restrictionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
@@ -24,25 +26,26 @@ class ActivityService
|
||||
* Add activity data to database.
|
||||
* @param Entity $entity
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param bool $extra
|
||||
* @param int $bookId
|
||||
* @param bool $extra
|
||||
*/
|
||||
public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
|
||||
{
|
||||
$this->activity->user_id = $this->user->id;
|
||||
$this->activity->book_id = $bookId;
|
||||
$this->activity->key = strtolower($activityKey);
|
||||
$activity = $this->activity->newInstance();
|
||||
$activity->user_id = $this->user->id;
|
||||
$activity->book_id = $bookId;
|
||||
$activity->key = strtolower($activityKey);
|
||||
if ($extra !== false) {
|
||||
$this->activity->extra = $extra;
|
||||
$activity->extra = $extra;
|
||||
}
|
||||
$entity->activity()->save($this->activity);
|
||||
$entity->activity()->save($activity);
|
||||
$this->setNotification($activityKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a activity history with a message & without binding to a entitiy.
|
||||
* Adds a activity history with a message & without binding to a entity.
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param int $bookId
|
||||
* @param bool|false $extra
|
||||
*/
|
||||
public function addMessage($activityKey, $bookId = 0, $extra = false)
|
||||
@@ -85,20 +88,22 @@ class ActivityService
|
||||
*/
|
||||
public function latest($count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->activity->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
$activityList = $this->restrictionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest activity for an entitiy, Filtering out similar
|
||||
* Gets the latest activity for an entity, Filtering out similar
|
||||
* items to prevent a message activity list.
|
||||
* @param Entity $entity
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
function entityActivity($entity, $count = 20, $page = 0)
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
@@ -107,15 +112,31 @@ class ActivityService
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out similar acitivity.
|
||||
* @param Activity[] $activity
|
||||
* Get latest activity for a user, Filtering out similar
|
||||
* items.
|
||||
* @param $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar($activity)
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
->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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out similar activity.
|
||||
* @param Activity[] $activities
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar($activities)
|
||||
{
|
||||
$newActivity = [];
|
||||
$previousItem = false;
|
||||
foreach ($activity as $activityItem) {
|
||||
foreach ($activities as $activityItem) {
|
||||
if ($previousItem === false) {
|
||||
$previousItem = $activityItem;
|
||||
$newActivity[] = $activityItem;
|
||||
|
||||
@@ -45,7 +45,7 @@ class EmailConfirmationService
|
||||
'token' => $token,
|
||||
]);
|
||||
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
|
||||
$appName = \Setting::get('app-name', 'BookStack');
|
||||
$appName = setting('app-name', 'BookStack');
|
||||
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
|
||||
});
|
||||
}
|
||||
|
||||
115
app/Services/ExportService.php
Normal file
115
app/Services/ExportService.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Page;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
|
||||
/**
|
||||
* Convert a page to a self-contained HTML file.
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a pdf file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
|
||||
$containedHtml = $this->containHtml($pageHtml);
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
return $pdf->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle of the contents of a html file to be self-contained.
|
||||
* @param $htmlContent
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function containHtml($htmlContent)
|
||||
{
|
||||
$imageTagsOutput = [];
|
||||
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$pathString = public_path($srcString);
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
$linksOutput = [];
|
||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
|
||||
foreach ($linksOutput[0] as $index => $linkMatch) {
|
||||
$oldLinkString = $linkMatch;
|
||||
$srcString = $linksOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$newSrcString = url($srcString);
|
||||
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
|
||||
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with system domain
|
||||
return $htmlContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the page contents into simple plain text.
|
||||
* This method filters any bad looking content to
|
||||
* provide a nice final output.
|
||||
* @param Page $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function pageToPlainText(Page $page)
|
||||
{
|
||||
$text = $page->text;
|
||||
// Replace multiple spaces with single spaces
|
||||
$text = preg_replace('/\ {2,}/', ' ', $text);
|
||||
// Reduce multiple horrid whitespace characters.
|
||||
$text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
|
||||
$text = html_entity_decode($text);
|
||||
// Add title
|
||||
$text = $page->name . "\n\n" . $text;
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Image;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
@@ -38,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +75,14 @@ class ImageService
|
||||
* @param string $imageName
|
||||
* @param string $imageData
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
private function saveNew($imageName, $imageData, $type)
|
||||
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$secureUploads = Setting::get('app-secure-images');
|
||||
$secureUploads = setting('app-secure-images');
|
||||
$imageName = str_replace(' ', '-', $imageName);
|
||||
|
||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
@@ -86,17 +93,27 @@ class ImageService
|
||||
}
|
||||
$fullPath = $imagePath . $imageName;
|
||||
|
||||
$storage->put($fullPath, $imageData);
|
||||
try {
|
||||
$storage->put($fullPath, $imageData);
|
||||
} catch (Exception $e) {
|
||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||
}
|
||||
|
||||
$userId = auth()->user()->id;
|
||||
$image = Image::forceCreate([
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
'type' => $type,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
'uploaded_to' => $uploadedTo
|
||||
];
|
||||
|
||||
if (auth()->user() && auth()->user()->id !== 0) {
|
||||
$userId = auth()->user()->id;
|
||||
$imageDetails['created_by'] = $userId;
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
|
||||
$image = Image::forceCreate($imageDetails);
|
||||
|
||||
return $image;
|
||||
}
|
||||
@@ -107,10 +124,12 @@ class ImageService
|
||||
* 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
|
||||
* @throws Exception
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
@@ -127,8 +146,16 @@ class ImageService
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
// Otherwise create the thumbnail
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($keepRatio) {
|
||||
$thumb->resize($width, null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
@@ -188,6 +215,7 @@ class ImageService
|
||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
$image->updated_by = $user->id;
|
||||
$image->save();
|
||||
return $image;
|
||||
}
|
||||
@@ -200,7 +228,7 @@ class ImageService
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = env('STORAGE_TYPE');
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
@@ -226,10 +254,10 @@ class ImageService
|
||||
private function getPublicUrl($filePath)
|
||||
{
|
||||
if ($this->storageUrl === null) {
|
||||
$storageUrl = env('STORAGE_URL');
|
||||
$storageUrl = config('filesystems.url');
|
||||
|
||||
// Get the standard public s3 url if s3 is set as storage type
|
||||
if ($storageUrl == false && env('STORAGE_TYPE') === 's3') {
|
||||
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
||||
$storageDetails = config('filesystems.disks.s3');
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
}
|
||||
|
||||
86
app/Services/Ldap.php
Normal file
86
app/Services/Ldap.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
/**
|
||||
* Class Ldap
|
||||
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
|
||||
* Allows the standard LDAP functions to be mocked for testing.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class Ldap
|
||||
{
|
||||
|
||||
/**
|
||||
* Connect to a LDAP server.
|
||||
* @param string $hostName
|
||||
* @param int $port
|
||||
* @return resource
|
||||
*/
|
||||
public function connect($hostName, $port)
|
||||
{
|
||||
return ldap_connect($hostName, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a LDAP option for the given connection.
|
||||
* @param resource $ldapConnection
|
||||
* @param int $option
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function setOption($ldapConnection, $option, $value)
|
||||
{
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search LDAP tree using the provided filter.
|
||||
* @param resource $ldapConnection
|
||||
* @param string $baseDn
|
||||
* @param string $filter
|
||||
* @param array|null $attributes
|
||||
* @return resource
|
||||
*/
|
||||
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
|
||||
{
|
||||
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries from an ldap search result.
|
||||
* @param resource $ldapConnection
|
||||
* @param resource $ldapSearchResult
|
||||
* @return array
|
||||
*/
|
||||
public function getEntries($ldapConnection, $ldapSearchResult)
|
||||
{
|
||||
return ldap_get_entries($ldapConnection, $ldapSearchResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and get entries immediately.
|
||||
* @param resource $ldapConnection
|
||||
* @param string $baseDn
|
||||
* @param string $filter
|
||||
* @param array|null $attributes
|
||||
* @return resource
|
||||
*/
|
||||
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
|
||||
{
|
||||
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
|
||||
return $this->getEntries($ldapConnection, $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to LDAP directory.
|
||||
* @param resource $ldapConnection
|
||||
* @param string $bindRdn
|
||||
* @param string $bindPassword
|
||||
* @return bool
|
||||
*/
|
||||
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
|
||||
{
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
}
|
||||
148
app/Services/LdapService.php
Normal file
148
app/Services/LdapService.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
* Handles any app-specific LDAP tasks.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class LdapService
|
||||
{
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
* @param Ldap $ldap
|
||||
*/
|
||||
public function __construct(Ldap $ldap)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->config = config('services.ldap');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of a user from LDAP using the given username.
|
||||
* User found via configurable user filter.
|
||||
* @param $userName
|
||||
* @return array|null
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserDetails($userName)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
|
||||
if ($users['count'] === 0) return null;
|
||||
|
||||
$user = $users[0];
|
||||
return [
|
||||
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
|
||||
'name' => $user['cn'][0],
|
||||
'dn' => $user['dn'],
|
||||
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Authenticatable $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(Authenticatable $user, $username, $password)
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) return false;
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
|
||||
} catch (\ErrorException $e) {
|
||||
$ldapBind = false;
|
||||
}
|
||||
|
||||
return $ldapBind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the system user to the LDAP connection using the given credentials
|
||||
* otherwise anonymous access is attempted.
|
||||
* @param $connection
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function bindSystemUser($connection)
|
||||
{
|
||||
$ldapDn = $this->config['dn'];
|
||||
$ldapPass = $this->config['pass'];
|
||||
|
||||
$isAnonymous = ($ldapDn === false || $ldapPass === false);
|
||||
if ($isAnonymous) {
|
||||
$ldapBind = $this->ldap->bind($connection);
|
||||
} else {
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection to the LDAP server.
|
||||
* Creates a new connection if one does not exist.
|
||||
* @return resource
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
if ($this->ldapConnection !== null) return $this->ldapConnection;
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
throw new LdapException('LDAP PHP extension not installed');
|
||||
}
|
||||
|
||||
// Get port from server string if specified.
|
||||
$ldapServer = explode(':', $this->config['server']);
|
||||
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a filter string by injecting common variables.
|
||||
* @param string $filterString
|
||||
* @param array $attrs
|
||||
* @return string
|
||||
*/
|
||||
protected function buildFilter($filterString, array $attrs)
|
||||
{
|
||||
$newAttrs = [];
|
||||
foreach ($attrs as $key => $attrText) {
|
||||
$newKey = '${' . $key . '}';
|
||||
$newAttrs[$newKey] = $attrText;
|
||||
}
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
|
||||
}
|
||||
326
app/Services/RestrictionService.php
Normal file
326
app/Services/RestrictionService.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Entity;
|
||||
|
||||
class RestrictionService
|
||||
{
|
||||
|
||||
protected $userRoles;
|
||||
protected $isAdmin;
|
||||
protected $currentAction;
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* RestrictionService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->currentUser = auth()->user();
|
||||
$this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
|
||||
$this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function checkIfEntityRestricted(Entity $entity, $action)
|
||||
{
|
||||
if ($this->isAdmin) return true;
|
||||
$this->currentAction = $action;
|
||||
$baseQuery = $entity->where('id', '=', $entity->id);
|
||||
if ($entity->isA('page')) {
|
||||
return $this->pageRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $this->bookRestrictionQuery($baseQuery)->count() > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a page query
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforcePageRestrictions($query, $action = 'view')
|
||||
{
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false);
|
||||
if ($this->currentUser) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->pageRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting pages.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function pageRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where(function ($query) {
|
||||
$query->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', false);
|
||||
})->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
})->where('restricted', '=', false);
|
||||
})->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)->where('chapter_id', '=', 0)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has no chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id');
|
||||
}, 'and', true)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has a chapter with accepted permissions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', true)
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'pages', 'Page');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add on permission restrictions to a chapter query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceChapterRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->chapterRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting chapters.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// Book & chapter unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where('restricted', '=', false)->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
})
|
||||
// Chapter unrestricted & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Chapter has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions to a book query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceBookRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->bookRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting books.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
$parentWhereQuery
|
||||
->where('restricted', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', true)->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
return $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
|
||||
->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->bookRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->chapterRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
return $query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The query to check the restrictions on an entity.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $modelName
|
||||
*/
|
||||
private function checkRestrictionsQuery($query, $tableName, $modelName)
|
||||
{
|
||||
$query->select('*')->from('restrictions')
|
||||
->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
|
||||
->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
|
||||
->where('restrictions.action', '=', $this->currentAction)
|
||||
->whereIn('restrictions.role_id', $this->userRoles);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -38,33 +38,52 @@ class SettingService
|
||||
*/
|
||||
public function get($key, $default = false)
|
||||
{
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
return $this->formatValue($value, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear an item from the cache completely.
|
||||
* @param $key
|
||||
*/
|
||||
protected function clearFromCache($key)
|
||||
{
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
@@ -136,9 +155,23 @@ class SettingService
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
private function getSettingObjectByKey($key)
|
||||
protected function getSettingObjectByKey($key)
|
||||
{
|
||||
return $this->setting->where('setting_key', '=', $key)->first();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an override value for a setting based on certain app conditions.
|
||||
* Used where certain configuration options overrule others.
|
||||
* Returns null if no override value is available.
|
||||
* @param $key
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function getOverrideValue($key)
|
||||
{
|
||||
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -76,9 +76,9 @@ class SocialAuthService
|
||||
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
|
||||
}
|
||||
|
||||
if($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException('The email '. $email.' is already in use. If you already have an account you can connect your ' . $socialDriver .' account from your profile settings.', '/login');
|
||||
throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
|
||||
}
|
||||
|
||||
return $socialUser;
|
||||
@@ -129,13 +129,13 @@ class SocialAuthService
|
||||
// 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 buy another user.');
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// Otherwise let the user know this social account is not used by anyone.
|
||||
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
|
||||
if (\Setting::get('registration-enabled')) {
|
||||
if (setting('registration-enabled')) {
|
||||
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
|
||||
}
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
@@ -172,9 +172,10 @@ class SocialAuthService
|
||||
*/
|
||||
private function checkDriverConfigured($driver)
|
||||
{
|
||||
$upperName = strtoupper($driver);
|
||||
$config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)];
|
||||
return (!in_array(false, $config) && !in_array(null, $config));
|
||||
$lowerName = strtolower($driver);
|
||||
$configPrefix = 'services.' . $lowerName . '.';
|
||||
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
||||
return !in_array(false, $config) && !in_array(null, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +194,7 @@ class SocialAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param string $socialDriver
|
||||
* @param \Laravel\Socialite\Contracts\User $socialUser
|
||||
* @return SocialAccount
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Entity;
|
||||
use BookStack\View;
|
||||
|
||||
@@ -9,15 +8,18 @@ class ViewService
|
||||
|
||||
protected $view;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param $view
|
||||
* @param View $view
|
||||
* @param RestrictionService $restrictionService
|
||||
*/
|
||||
public function __construct(View $view)
|
||||
public function __construct(View $view, RestrictionService $restrictionService)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->restrictionService = $restrictionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +29,7 @@ class ViewService
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
if($this->user === null) return 0;
|
||||
if ($this->user === null) return 0;
|
||||
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
|
||||
// Add view if model exists
|
||||
if ($view) {
|
||||
@@ -44,52 +46,47 @@ class ViewService
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false $filterModel
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
$query = $this->restrictionService->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) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
|
||||
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable()->getResults();
|
||||
});
|
||||
return $viewedEntities;
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recently viewed entities for the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param Entity|bool $filterModel
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
if($this->user === null) return collect();
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->view->where('user_id', '=', auth()->user()->id);
|
||||
if ($this->user === null) return collect();
|
||||
|
||||
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $this->restrictionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable()->getResults();
|
||||
});
|
||||
return $viewedEntities;
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('user_id', '=', auth()->user()->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||
return $viewables;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset all view counts by deleting all views.
|
||||
*/
|
||||
@@ -98,5 +95,4 @@ class ViewService
|
||||
$this->view->truncate();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
63
app/User.php
63
app/User.php
@@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* The database table used by the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'password', 'image_id'];
|
||||
protected $fillable = ['name', 'email', 'image_id'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
@@ -50,10 +47,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions and roles
|
||||
*/
|
||||
|
||||
/**
|
||||
* The roles that belong to the user.
|
||||
*/
|
||||
@@ -62,21 +55,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
}
|
||||
|
||||
public function getRoleAttribute()
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasRole($role)
|
||||
{
|
||||
return $this->roles()->with('permissions')->first();
|
||||
return $this->roles->pluck('name')->contains($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user's permissions from thier role.
|
||||
* Get all permissions belonging to a the current user.
|
||||
* @param bool $cache
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
|
||||
*/
|
||||
private function loadPermissions()
|
||||
public function permissions($cache = true)
|
||||
{
|
||||
if (isset($this->permissions)) return;
|
||||
if(isset($this->permissions) && $cache) return $this->permissions;
|
||||
$this->load('roles.permissions');
|
||||
$permissions = $this->roles[0]->permissions;
|
||||
$permissionsArray = $permissions->pluck('name')->all();
|
||||
$this->permissions = $permissionsArray;
|
||||
$permissions = $this->roles->map(function($role) {
|
||||
return $role->permissions;
|
||||
})->flatten()->unique();
|
||||
$this->permissions = $permissions;
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,11 +88,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function can($permissionName)
|
||||
{
|
||||
if ($this->email == 'guest') {
|
||||
return false;
|
||||
}
|
||||
$this->loadPermissions();
|
||||
return array_search($permissionName, $this->permissions) !== false;
|
||||
if ($this->email === 'guest') return false;
|
||||
return $this->permissions()->pluck('name')->contains($permissionName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,12 +107,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function attachRoleId($id)
|
||||
{
|
||||
$this->roles()->sync([$id]);
|
||||
$this->roles()->attach($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the social account associated with this user.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function socialAccounts()
|
||||
@@ -138,8 +136,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Returns the user's avatar,
|
||||
* Uses Gravatar as the avatar service.
|
||||
*
|
||||
* @param int $size
|
||||
* @return string
|
||||
*/
|
||||
@@ -164,6 +160,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return '/users/' . $this->id;
|
||||
return '/settings/users/' . $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,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('versioned_asset')) {
|
||||
if (!function_exists('versioned_asset')) {
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
@@ -27,4 +27,48 @@ if (! function_exists('versioned_asset')) {
|
||||
|
||||
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
|
||||
* that particular item.
|
||||
* @param $permission
|
||||
* @param \BookStack\Ownable $ownable
|
||||
* @return mixed
|
||||
*/
|
||||
function userCan($permission, \BookStack\Ownable $ownable = null)
|
||||
{
|
||||
if (!auth()->check()) return false;
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissionBaseName = strtolower($permission) . '-';
|
||||
$hasPermission = false;
|
||||
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
|
||||
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
|
||||
|
||||
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
|
||||
|
||||
// Check restrictions on the entity
|
||||
$restrictionService = app('BookStack\Services\RestrictionService');
|
||||
$explodedPermission = explode('-', $permission);
|
||||
$action = end($explodedPermission);
|
||||
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
|
||||
$restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
|
||||
return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to access system settings.
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return mixed
|
||||
*/
|
||||
function setting($key, $default = false)
|
||||
{
|
||||
$settingService = app('BookStack\Services\SettingService');
|
||||
return $settingService->get($key, $default);
|
||||
}
|
||||
|
||||
@@ -6,18 +6,22 @@
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"laravel/framework": "5.1.*",
|
||||
"laravel/framework": "5.2.*",
|
||||
"intervention/image": "^2.3",
|
||||
"laravel/socialite": "^2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"barryvdh/laravel-debugbar": "^2.0",
|
||||
"league/flysystem-aws-s3-v3": "^1.0"
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "0.6.*",
|
||||
"predis/predis": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"phpspec/phpspec": "~2.1"
|
||||
"phpspec/phpspec": "~2.1",
|
||||
"symfony/dom-crawler": "~3.0",
|
||||
"symfony/css-selector": "~3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
||||
1337
composer.lock
generated
1337
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
'editor' => env('APP_EDITOR', 'html'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
@@ -113,13 +118,11 @@ return [
|
||||
/*
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
||||
Illuminate\Bus\BusServiceProvider::class,
|
||||
Illuminate\Cache\CacheServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
||||
Illuminate\Routing\ControllerServiceProvider::class,
|
||||
Illuminate\Cookie\CookieServiceProvider::class,
|
||||
Illuminate\Database\DatabaseServiceProvider::class,
|
||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
||||
@@ -142,6 +145,7 @@ return [
|
||||
* Third Party
|
||||
*/
|
||||
Intervention\Image\ImageServiceProvider::class,
|
||||
Barryvdh\DomPDF\ServiceProvider::class,
|
||||
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
|
||||
Barryvdh\Debugbar\ServiceProvider::class,
|
||||
|
||||
@@ -149,6 +153,7 @@ return [
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
BookStack\Providers\RouteServiceProvider::class,
|
||||
@@ -208,6 +213,7 @@ return [
|
||||
*/
|
||||
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'PDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'Debugbar' => Barryvdh\Debugbar\Facade::class,
|
||||
|
||||
/**
|
||||
|
||||
119
config/auth.php
119
config/auth.php
@@ -2,66 +2,109 @@
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'method' => env('AUTH_METHOD', 'standard'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Authentication Driver
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the authentication driver that will be utilized.
|
||||
| This driver manages the retrieval and authentication of the users
|
||||
| attempting to get access to protected areas of your application.
|
||||
| This option controls the default authentication "guard" and password
|
||||
| reset options for your application. You may change these defaults
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => 'web',
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| here which uses session storage and the Eloquent user provider.
|
||||
|
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
|
|
||||
| Supported: "session", "token"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'token',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| sources which represent each model / table. These sources may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => 'eloquent',
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
|
||||
'model' => BookStack\User::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Model
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "Eloquent" authentication driver, we need to know which
|
||||
| Eloquent model should be used to retrieve your users. Of course, it
|
||||
| is often just the "User" model but you may use whatever you like.
|
||||
|
|
||||
*/
|
||||
|
||||
'model' => BookStack\User::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "Database" authentication driver, we need to know which
|
||||
| table should be used to retrieve your users. We have chosen a basic
|
||||
| default value but you may easily change it to any table you like.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => 'users',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Settings
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may set the options for resetting passwords including the view
|
||||
| that is your password reset e-mail. You can also set the name of the
|
||||
| that is your password reset e-mail. You may also set the name of the
|
||||
| table that maintains all of the reset tokens for your application.
|
||||
|
|
||||
| You may specify multiple password reset configurations if you have more
|
||||
| than one user table or model in the application and you want to have
|
||||
| separate password reset settings based on the specific user types.
|
||||
|
|
||||
| The expire time is the number of minutes that the reset token should be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'password' => [
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
@@ -1,5 +1,17 @@
|
||||
<?php
|
||||
|
||||
// MEMCACHED - Split out configuration into an array
|
||||
if (env('CACHE_DRIVER') === 'memcached') {
|
||||
$memcachedServerKeys = ['host', 'port', 'weight'];
|
||||
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
|
||||
foreach ($memcachedServers as $index => $memcachedServer) {
|
||||
$memcachedServerDetails = explode(':', $memcachedServer);
|
||||
if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
|
||||
if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
|
||||
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -49,11 +61,7 @@ return [
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'servers' => [
|
||||
[
|
||||
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
|
||||
],
|
||||
],
|
||||
'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
@@ -74,6 +82,6 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => 'laravel',
|
||||
'prefix' => env('CACHE_PREFIX', 'bookstack'),
|
||||
|
||||
];
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
<?php
|
||||
|
||||
// REDIS - Split out configuration into an array
|
||||
if (env('REDIS_SERVERS', false)) {
|
||||
$redisServerKeys = ['host', 'port', 'database'];
|
||||
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
|
||||
$redisConfig = [
|
||||
'cluster' => env('REDIS_CLUSTER', false)
|
||||
];
|
||||
foreach ($redisServers as $index => $redisServer) {
|
||||
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
|
||||
$redisServerDetails = explode(':', $redisServer);
|
||||
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
|
||||
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
|
||||
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -123,16 +139,6 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'cluster' => false,
|
||||
|
||||
'default' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'database' => 0,
|
||||
],
|
||||
|
||||
],
|
||||
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
|
||||
|
||||
];
|
||||
|
||||
266
config/dompdf.php
Normal file
266
config/dompdf.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set some default values. It is possible to add all defines that can be set
|
||||
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||
|
|
||||
*/
|
||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||
'orientation' => 'portrait',
|
||||
'defines' => array(
|
||||
/**
|
||||
* The location of the DOMPDF font directory
|
||||
*
|
||||
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
* *Please note the trailing slash.*
|
||||
*
|
||||
* Notes regarding fonts:
|
||||
* Additional .afm font metrics can be added by executing load_font.php from command line.
|
||||
*
|
||||
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
|
||||
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
|
||||
* increase file size unless font subsetting is enabled. Before embedding a font please
|
||||
* review your rights under the font license.
|
||||
*
|
||||
* Any font specification in the source HTML is translated to the closest font available
|
||||
* in the font directory.
|
||||
*
|
||||
* The pdf standard "Base 14 fonts" are:
|
||||
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
|
||||
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* Symbol, ZapfDingbats.
|
||||
*/
|
||||
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
|
||||
/**
|
||||
* The location of the DOMPDF font cache directory
|
||||
*
|
||||
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||
* This directory can be the same as DOMPDF_FONT_DIR
|
||||
*
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
*/
|
||||
"DOMPDF_FONT_CACHE" => storage_path('fonts/'),
|
||||
|
||||
/**
|
||||
* The location of a temporary directory.
|
||||
*
|
||||
* The directory specified must be writeable by the webserver process.
|
||||
* The temporary directory is required to download remote images and when
|
||||
* using the PFDLib back end.
|
||||
*/
|
||||
"DOMPDF_TEMP_DIR" => sys_get_temp_dir(),
|
||||
|
||||
/**
|
||||
* ==== IMPORTANT ====
|
||||
*
|
||||
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||
* files on the webserver. All local files opened by dompdf must be in a
|
||||
* subdirectory of this directory. DO NOT set it to '/' since this could
|
||||
* allow an attacker to use dompdf to read any files on the server. This
|
||||
* should be an absolute path.
|
||||
* This is only checked on command line call by dompdf.php, but not by
|
||||
* direct class use like:
|
||||
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||
*/
|
||||
"DOMPDF_CHROOT" => realpath(base_path()),
|
||||
|
||||
/**
|
||||
* Whether to use Unicode fonts or not.
|
||||
*
|
||||
* When set to true the PDF backend must be set to "CPDF" and fonts must be
|
||||
* loaded via load_font.php.
|
||||
*
|
||||
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
|
||||
* document must be present in your fonts, however.
|
||||
*/
|
||||
"DOMPDF_UNICODE_ENABLED" => true,
|
||||
|
||||
/**
|
||||
* Whether to enable font subsetting or not.
|
||||
*/
|
||||
"DOMPDF_ENABLE_FONTSUBSETTING" => false,
|
||||
|
||||
/**
|
||||
* The PDF rendering backend to use
|
||||
*
|
||||
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
|
||||
* Canvas_Factory} ultimately determines which rendering class to instantiate
|
||||
* based on this setting.
|
||||
*
|
||||
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||
* capabilities for dompdf, however additional features (e.g. object,
|
||||
* image and font support, etc.) differ between backends. Please see
|
||||
* {@link PDFLib_Adapter} for more information on the PDFLib backend
|
||||
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
|
||||
* on CPDF. Also see the documentation for each backend at the links
|
||||
* below.
|
||||
*
|
||||
* The GD rendering backend is a little different than PDFLib and
|
||||
* CPDF. Several features of CPDF and PDFLib are not supported or do
|
||||
* not make any sense when creating image files. For example,
|
||||
* multiple pages are not supported, nor are PDF 'objects'. Have a
|
||||
* look at {@link GD_Adapter} for more information. GD support is
|
||||
* experimental, so use it at your own risk.
|
||||
*
|
||||
* @link http://www.pdflib.com
|
||||
* @link http://www.ros.co.nz/pdf
|
||||
* @link http://www.php.net/image
|
||||
*/
|
||||
"DOMPDF_PDF_BACKEND" => "CPDF",
|
||||
|
||||
/**
|
||||
* PDFlib license key
|
||||
*
|
||||
* If you are using a licensed, commercial version of PDFlib, specify
|
||||
* your license key here. If you are using PDFlib-Lite or are evaluating
|
||||
* the commercial version of PDFlib, comment out this setting.
|
||||
*
|
||||
* @link http://www.pdflib.com
|
||||
*
|
||||
* If pdflib present in web server and auto or selected explicitely above,
|
||||
* a real license code must exist!
|
||||
*/
|
||||
//"DOMPDF_PDFLIB_LICENSE" => "your license key here",
|
||||
|
||||
/**
|
||||
* html target media view which should be rendered into pdf.
|
||||
* List of types and parsing rules for future extensions:
|
||||
* http://www.w3.org/TR/REC-html40/types.html
|
||||
* screen, tty, tv, projection, handheld, print, braille, aural, all
|
||||
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
|
||||
* Note, even though the generated pdf file is intended for print output,
|
||||
* the desired content might be different (e.g. screen or projection view of html file).
|
||||
* Therefore allow specification of content here.
|
||||
*/
|
||||
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen",
|
||||
|
||||
/**
|
||||
* The default paper size.
|
||||
*
|
||||
* North America standard is "letter"; other countries generally "a4"
|
||||
*
|
||||
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||
*/
|
||||
"DOMPDF_DEFAULT_PAPER_SIZE" => "a4",
|
||||
|
||||
/**
|
||||
* The default font family
|
||||
*
|
||||
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||
* @var string
|
||||
*/
|
||||
"DOMPDF_DEFAULT_FONT" => "dejavu sans",
|
||||
|
||||
/**
|
||||
* Image DPI setting
|
||||
*
|
||||
* This setting determines the default DPI setting for images and fonts. The
|
||||
* DPI may be overridden for inline images by explictly setting the
|
||||
* image's width & height style attributes (i.e. if the image's native
|
||||
* width is 600 pixels and you specify the image's width as 72 points,
|
||||
* the image will have a DPI of 600 in the rendered PDF. The DPI of
|
||||
* background images can not be overridden and is controlled entirely
|
||||
* via this parameter.
|
||||
*
|
||||
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
|
||||
* If a size in html is given as px (or without unit as image size),
|
||||
* this tells the corresponding size in pt.
|
||||
* This adjusts the relative sizes to be similar to the rendering of the
|
||||
* html page in a reference browser.
|
||||
*
|
||||
* In pdf, always 1 pt = 1/72 inch
|
||||
*
|
||||
* Rendering resolution of various browsers in px per inch:
|
||||
* Windows Firefox and Internet Explorer:
|
||||
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
|
||||
* Linux Firefox:
|
||||
* about:config *resolution: Default:96
|
||||
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
|
||||
*
|
||||
* Take care about extra font/image zoom factor of browser.
|
||||
*
|
||||
* In images, <img> size in pixel attribute, img css style, are overriding
|
||||
* the real image dimension in px for rendering.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
"DOMPDF_DPI" => 96,
|
||||
|
||||
/**
|
||||
* Enable inline PHP
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically evaluate
|
||||
* inline PHP contained within <script type="text/php"> ... </script> tags.
|
||||
*
|
||||
* Enabling this for documents you do not trust (e.g. arbitrary remote html
|
||||
* pages) is a security risk. Set this option to false if you wish to process
|
||||
* untrusted documents.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_PHP" => false,
|
||||
|
||||
/**
|
||||
* Enable inline Javascript
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically insert
|
||||
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => true,
|
||||
|
||||
/**
|
||||
* Enable remote file access
|
||||
*
|
||||
* If this setting is set to true, DOMPDF will access remote sites for
|
||||
* images and CSS files as required.
|
||||
* This is required for part of test case www/test/image_variants.html through www/examples.php
|
||||
*
|
||||
* Attention!
|
||||
* This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
|
||||
* allowing remote access to dompdf.php or on allowing remote html code to be passed to
|
||||
* $dompdf = new DOMPDF(, $dompdf->load_html(...,
|
||||
* This allows anonymous users to download legally doubtful internet content which on
|
||||
* tracing back appears to being downloaded by your server, or allows malicious php code
|
||||
* in remote html pages to be executed by your server with your account privileges.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_REMOTE" => true,
|
||||
|
||||
/**
|
||||
* A ratio applied to the fonts height to be more like browsers' line height
|
||||
*/
|
||||
"DOMPDF_FONT_HEIGHT_RATIO" => 1.1,
|
||||
|
||||
/**
|
||||
* Enable CSS float
|
||||
*
|
||||
* Allows people to disabled CSS float support
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_CSS_FLOAT" => true,
|
||||
|
||||
|
||||
/**
|
||||
* Use the more-than-experimental HTML5 Lib parser
|
||||
*/
|
||||
"DOMPDF_ENABLE_HTML5PARSER" => true,
|
||||
|
||||
|
||||
),
|
||||
|
||||
|
||||
);
|
||||
@@ -15,7 +15,18 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 'local',
|
||||
'default' => env('STORAGE_TYPE', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Storage URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the url to where the storage is located for when using an external
|
||||
| file storage service, such as s3, to store publicly accessible assets.
|
||||
|
|
||||
*/
|
||||
'url' => env('STORAGE_URL', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -13,6 +13,8 @@ return [
|
||||
| to have a conventional place to find your various credentials.
|
||||
|
|
||||
*/
|
||||
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
|
||||
'callback_url' => env('APP_URL', false),
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => '',
|
||||
@@ -47,4 +49,13 @@ return [
|
||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
||||
],
|
||||
|
||||
'ldap' => [
|
||||
'server' => env('LDAP_SERVER', false),
|
||||
'dn' => env('LDAP_DN', false),
|
||||
'pass' => env('LDAP_PASS', false),
|
||||
'base_dn' => env('LDAP_BASE_DN', false),
|
||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
||||
'version' => env('LDAP_VERSION', false)
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
10
config/setting-defaults.php
Normal file
10
config/setting-defaults.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* The defaults for the system settings that are saved in the database.
|
||||
*/
|
||||
return [
|
||||
|
||||
'app-editor' => 'wysiwyg'
|
||||
|
||||
];
|
||||
@@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) {
|
||||
'email' => $faker->email,
|
||||
'password' => str_random(10),
|
||||
'remember_token' => str_random(10),
|
||||
'email_confirmed' => 1
|
||||
];
|
||||
});
|
||||
|
||||
@@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) {
|
||||
'text' => strip_tags($html)
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Role::class, function ($faker) {
|
||||
return [
|
||||
'display_name' => $faker->sentence(3),
|
||||
'description' => $faker->sentence(10)
|
||||
];
|
||||
});
|
||||
@@ -18,13 +18,13 @@ class CreateUsersTable extends Migration
|
||||
$table->string('email')->unique();
|
||||
$table->string('password', 60);
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
\BookStack\User::create([
|
||||
\BookStack\User::forceCreate([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => \Illuminate\Support\Facades\Hash::make('password')
|
||||
'password' => bcrypt('password')
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class CreateBooksTable extends Migration
|
||||
$table->string('name');
|
||||
$table->string('slug')->indexed();
|
||||
$table->text('description');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class CreatePagesTable extends Migration
|
||||
$table->longText('html');
|
||||
$table->longText('text');
|
||||
$table->integer('priority');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class CreateImagesTable extends Migration
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->string('url');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class CreateChaptersTable extends Migration
|
||||
$table->text('name');
|
||||
$table->text('description');
|
||||
$table->integer('priority');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration
|
||||
$table->longText('html');
|
||||
$table->longText('text');
|
||||
$table->integer('created_by');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration
|
||||
$table->integer('user_id');
|
||||
$table->integer('entity_id');
|
||||
$table->string('entity_type');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class AddRolesAndPermissions extends Migration
|
||||
$table->string('name')->unique();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
// Create table for associating roles to users (Many-to-Many)
|
||||
@@ -50,7 +50,7 @@ class AddRolesAndPermissions extends Migration
|
||||
$table->string('name')->unique();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
// Create table for associating permissions to roles (Many-to-Many)
|
||||
|
||||
@@ -15,7 +15,7 @@ class CreateSettingsTable extends Migration
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
$table->string('setting_key')->primary()->indexed();
|
||||
$table->text('value');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration
|
||||
$table->string('driver')->index();
|
||||
$table->string('driver_id');
|
||||
$table->string('avatar');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration
|
||||
$table->increments('id');
|
||||
$table->integer('user_id')->index();
|
||||
$table->string('token')->index();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateViewsTable extends Migration
|
||||
$table->integer('viewable_id');
|
||||
$table->string('viewable_type');
|
||||
$table->integer('views');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddExternalAuthToUsers extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('external_auth_id')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('external_auth_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSlugToRevisions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->string('slug');
|
||||
$table->index('slug');
|
||||
$table->string('book_slug');
|
||||
$table->index('book_slug');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->dropColumn('slug');
|
||||
$table->dropColumn('book_slug');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdatePermissionsAndRoles extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
$editorRole = \BookStack\Role::getRole('editor');
|
||||
|
||||
// Delete old permissions
|
||||
$permissions = \BookStack\Permission::all();
|
||||
$permissions->each(function ($permission) {
|
||||
$permission->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'
|
||||
];
|
||||
foreach ($permissionsToCreate as $name => $displayName) {
|
||||
$newPermission = new \BookStack\Permission();
|
||||
$newPermission->name = $name;
|
||||
$newPermission->display_name = $displayName;
|
||||
$newPermission->save();
|
||||
$adminRole->attachPermission($newPermission);
|
||||
}
|
||||
|
||||
// Create & attach new entity permissions
|
||||
$entities = ['Book', 'Page', 'Chapter', 'Image'];
|
||||
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($ops as $op) {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Get roles with permissions we need to change
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
|
||||
// Delete old permissions
|
||||
$permissions = \BookStack\Permission::all();
|
||||
$permissions->each(function ($permission) {
|
||||
$permission->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);
|
||||
}
|
||||
}
|
||||
|
||||
// Create admin permissions
|
||||
$entities = ['Settings', 'User'];
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddEntityAccessControls extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('images', function (Blueprint $table) {
|
||||
$table->integer('uploaded_to')->default(0);
|
||||
$table->index('uploaded_to');
|
||||
});
|
||||
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::create('restrictions', function(Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('restrictable_id');
|
||||
$table->string('restrictable_type');
|
||||
$table->integer('role_id');
|
||||
$table->string('action');
|
||||
$table->index('role_id');
|
||||
$table->index('action');
|
||||
$table->index(['restrictable_id', 'restrictable_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('images', function (Blueprint $table) {
|
||||
$table->dropColumn('uploaded_to');
|
||||
});
|
||||
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->dropColumn('restricted');
|
||||
});
|
||||
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->dropColumn('restricted');
|
||||
});
|
||||
|
||||
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('restricted');
|
||||
});
|
||||
|
||||
Schema::drop('restrictions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddPageRevisionTypes extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->string('type')->default('version');
|
||||
$table->index('type');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->dropColumn('type');
|
||||
});
|
||||
}
|
||||
}
|
||||
32
database/migrations/2016_03_13_082138_add_page_drafts.php
Normal file
32
database/migrations/2016_03_13_082138_add_page_drafts.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddPageDrafts extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('pages', function(Blueprint $table) {
|
||||
$table->boolean('draft')->default(false);
|
||||
$table->index('draft');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('draft');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddMarkdownSupport extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->longText('markdown')->default('');
|
||||
});
|
||||
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->longText('markdown')->default('');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('markdown');
|
||||
});
|
||||
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->dropColumn('markdown');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
|
||||
public function run()
|
||||
{
|
||||
$user = factory(BookStack\User::class, 1)->create();
|
||||
$role = \BookStack\Role::where('name', '=', 'admin')->first();
|
||||
$role = \BookStack\Role::getRole('editor');
|
||||
$user->attachRole($role);
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) {
|
||||
elixir(function(mix) {
|
||||
mix.sass('styles.scss')
|
||||
.sass('print-styles.scss')
|
||||
.browserify(['jquery-extensions.js', 'global.js'], 'public/js/common.js')
|
||||
.sass('export-styles.scss')
|
||||
.browserify('global.js', 'public/js/common.js')
|
||||
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
|
||||
});
|
||||
|
||||
14
package.json
14
package.json
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.9.0",
|
||||
"insert-css": "^0.2.0"
|
||||
"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",
|
||||
"babel-runtime": "^5.8.29",
|
||||
"bootstrap-sass": "^3.0.0",
|
||||
"dropzone": "^4.0.1",
|
||||
"laravel-elixir": "^3.4.0",
|
||||
"vue": "^1.0.4",
|
||||
"vue-hot-reload-api": "^1.2.1",
|
||||
"vue-resource": "^0.1.16",
|
||||
"vueify": "^5.0.1",
|
||||
"vueify-insert-css": "^1.0.0",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.12.0",
|
||||
"zeroclipboard": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
11
phpunit.xml
11
phpunit.xml
@@ -21,11 +21,18 @@
|
||||
</filter>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_DEBUG" value="false"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||
<env name="MAIL_PRETEND" value="true"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||
<env name="MAIL_DRIVER" value="log"/>
|
||||
<env name="AUTH_METHOD" value="standard"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
||||
<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"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
2
public/build/.gitignore
vendored
Normal file
2
public/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"css/styles.css": "css/styles.css?version=bdb0019",
|
||||
"css/print-styles.css": "css/print-styles.css?version=bdb0019",
|
||||
"js/common.js": "js/common.js?version=bdb0019"
|
||||
"css/styles.css": "css/styles.css?version=b4531da",
|
||||
"css/print-styles.css": "css/print-styles.css?version=b4531da",
|
||||
"js/common.js": "js/common.js?version=b4531da"
|
||||
}
|
||||
1
public/css/export-styles.css
vendored
Normal file
1
public/css/export-styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/css/styles.css
vendored
2
public/css/styles.css
vendored
File diff suppressed because one or more lines are too long
BIN
public/fonts/roboto-bold-webfont.eot
Normal file
BIN
public/fonts/roboto-bold-webfont.eot
Normal file
Binary file not shown.
675
public/fonts/roboto-bold-webfont.svg
Normal file
675
public/fonts/roboto-bold-webfont.svg
Normal file
@@ -0,0 +1,675 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 75 KiB |
BIN
public/fonts/roboto-bold-webfont.ttf
Normal file
BIN
public/fonts/roboto-bold-webfont.ttf
Normal file
Binary file not shown.
BIN
public/fonts/roboto-bold-webfont.woff
Normal file
BIN
public/fonts/roboto-bold-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/roboto-bold-webfont.woff2
Normal file
BIN
public/fonts/roboto-bold-webfont.woff2
Normal file
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