Merge branch 'master' into release and updated version

This commit is contained in:
Dan Brown 2018-02-11 18:19:38 +00:00
commit ac3ba594a4
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
160 changed files with 4057 additions and 760 deletions

View File

@ -47,10 +47,15 @@ GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
OKTA_BASE_URL=false
OKTA_KEY=false
OKTA_SECRET=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
# External services such as Gravatar
# External services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
@ -67,4 +72,4 @@ MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_ENCRYPTION=null

View File

@ -16,7 +16,9 @@ class Activity extends Model
*/
public function entity()
{
if ($this->entity_type === '') $this->entity_type = null;
if ($this->entity_type === '') {
$this->entity_type = null;
}
return $this->morphTo('entity');
}
@ -43,8 +45,8 @@ class Activity extends Model
* @param $activityB
* @return bool
*/
public function isSimilarTo($activityB) {
public function isSimilarTo($activityB)
{
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class Attachment extends Ownable
{
protected $fillable = ['name', 'order'];
@ -11,7 +10,9 @@ class Attachment extends Ownable
*/
public function getFileName()
{
if (str_contains($this->name, '.')) return $this->name;
if (str_contains($this->name, '.')) {
return $this->name;
}
return $this->name . '.' . $this->extension;
}
@ -32,5 +33,4 @@ class Attachment extends Ownable
{
return baseUrl('/attachments/' . $this->id);
}
}

View File

@ -27,7 +27,9 @@ class Book extends Entity
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) return $default;
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
@ -91,5 +93,4 @@ class Book extends Entity
{
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class Chapter extends Entity
{
protected $fillable = ['name', 'description', 'priority', 'book_id'];
@ -59,5 +58,4 @@ class Chapter extends Entity
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Repos\UserRepo;
use Illuminate\Console\Command;
class CreateAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:create-admin
{--email= : The email address for the new admin user}
{--name= : The name of the new admin user}
{--password= : The password to assign to the new admin user}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new admin user to the system';
protected $userRepo;
/**
* Create a new command instance.
*
* @param UserRepo $userRepo
*/
public function __construct(UserRepo $userRepo)
{
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function handle()
{
$email = trim($this->option('email'));
if (empty($email)) {
$email = $this->ask('Please specify an email address for the new admin user');
}
if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->error('Invalid email address provided');
}
if ($this->userRepo->getByEmail($email) !== null) {
return $this->error('A user with the provided email already exists!');
}
$name = trim($this->option('name'));
if (empty($name)) {
$name = $this->ask('Please specify an name for the new admin user');
}
if (strlen($name) < 2) {
return $this->error('Invalid name provided');
}
$password = trim($this->option('password'));
if (empty($password)) {
$password = $this->secret('Please specify a password for the new admin user');
}
if (strlen($password) < 5) {
return $this->error('Invalid password provided, Must be at least 5 characters');
}
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
$this->userRepo->attachSystemRole($user, 'admin');
$this->userRepo->downloadGravatarToUserAvatar($user);
$user->email_confirmed = true;
$user->save();
$this->info("Admin account with email \"{$user->email}\" successfully created!");
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\User;
use BookStack\Repos\UserRepo;
use Illuminate\Console\Command;
class DeleteUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:delete-users';
protected $user;
protected $userRepo;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete users that are not "admin" or system users.';
public function __construct(User $user, UserRepo $userRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
parent::__construct();
}
public function handle()
{
$confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
$numDeleted = 0;
if (strtolower(trim($confirm)) === 'yes') {
$totalUsers = $this->user->count();
$users = $this->user->where('system_name', '=', null)->with('roles')->get();
foreach ($users as $user) {
if ($user->hasSystemRole('admin')) {
// don't delete users with "admin" role
continue;
}
$this->userRepo->destroy($user);
++$numDeleted;
}
$this->info("Deleted $numDeleted of $totalUsers total users.");
} else {
$this->info('Exiting...');
}
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Entity extends Ownable
@ -28,7 +27,9 @@ class Entity extends Ownable
{
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
if ($matches) return true;
if ($matches) {
return true;
}
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
return $entity->book_id === $this->id;
@ -159,7 +160,9 @@ class Entity extends Ownable
*/
public function getShortName($length = 25)
{
if (strlen($this->name) <= $length) return $this->name;
if (strlen($this->name) <= $length) {
return $this->name;
}
return substr($this->name, 0, $length - 3) . '...';
}
@ -176,13 +179,18 @@ class Entity extends Ownable
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery(){return '';}
public function entityRawQuery()
{
return '';
}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path){return '/';}
public function getUrl($path)
{
return '/';
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class EntityPermission extends Model
{

View File

@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class AuthException extends PrettyException
{
class AuthException extends PrettyException {}
}

View File

@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class ConfirmationEmailException extends NotifyException
{
class ConfirmationEmailException extends NotifyException {}
}

View File

@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class FileUploadException extends PrettyException
{
class FileUploadException extends PrettyException {}
}

View File

@ -4,11 +4,14 @@ namespace BookStack\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Handler extends ExceptionHandler
{
@ -60,18 +63,44 @@ class Handler extends ExceptionHandler
return response()->view('errors/' . $code, ['message' => $message], $code);
}
// Handle 404 errors with a loaded session to enable showing user-specific information
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
return $this->loadErrorMiddleware($request, function ($request) use ($e) {
$message = $e->getMessage() ?: trans('errors.404_page_not_found');
return response()->view('errors/404', ['message' => $message], 404);
});
}
return parent::render($request, $e);
}
/**
* Load the middleware required to show state/session-enabled error pages.
* @param Request $request
* @param $callback
* @return mixed
*/
protected function loadErrorMiddleware(Request $request, $callback)
{
$middleware = (\Route::getMiddlewareGroups()['web_errors']);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then($callback);
}
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e
* @param $type
* @return bool
*/
protected function isExceptionType(Exception $e, $type) {
protected function isExceptionType(Exception $e, $type)
{
do {
if (is_a($e, $type)) return true;
if (is_a($e, $type)) {
return true;
}
} while ($e = $e->getPrevious());
return false;
}
@ -81,7 +110,8 @@ class Handler extends ExceptionHandler
* @param Exception $e
* @return string
*/
protected function getOriginalMessage(Exception $e) {
protected function getOriginalMessage(Exception $e)
{
do {
$message = $e->getMessage();
} while ($e = $e->getPrevious());

View File

@ -1,3 +1,6 @@
<?php namespace BookStack\Exceptions;
class ImageUploadException extends PrettyException {}
class ImageUploadException extends PrettyException
{
}

View File

@ -1,3 +1,6 @@
<?php namespace BookStack\Exceptions;
class LdapException extends PrettyException {}
class LdapException extends PrettyException
{
}

View File

@ -1,7 +1,7 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
class NotFoundException extends PrettyException
{
/**
* NotFoundException constructor.
@ -11,4 +11,4 @@ class NotFoundException extends PrettyException {
{
parent::__construct($message, 404);
}
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Exceptions;
class NotifyException extends \Exception
{
@ -18,4 +17,4 @@ class NotifyException extends \Exception
$this->redirectLocation = $redirectLocation;
parent::__construct();
}
}
}

View File

@ -1,6 +1,8 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PermissionsException extends Exception {}
class PermissionsException extends Exception
{
}

View File

@ -1,3 +1,6 @@
<?php namespace BookStack\Exceptions;
class PrettyException extends \Exception {}
class PrettyException extends \Exception
{
}

View File

@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialDriverNotConfigured extends PrettyException
{
class SocialDriverNotConfigured extends PrettyException {}
}

View File

@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialSignInException extends NotifyException
{
class SocialSignInException extends NotifyException {}
}

View File

@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class UserRegistrationException extends NotifyException
{
class UserRegistrationException extends NotifyException {}
}

View File

@ -2,6 +2,7 @@
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Services\AttachmentService;
use Illuminate\Http\Request;
@ -182,11 +183,16 @@ class AttachmentController extends Controller
* Get an attachment from storage.
* @param $attachmentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function get($attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
if ($page === null) {
throw new NotFoundException(trans('errors.attachment_not_found'));
}
$this->checkOwnablePermission('page-view', $page);
if ($attachment->external) {
@ -204,6 +210,7 @@ class AttachmentController extends Controller
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
* @throws \Exception
*/
public function delete($attachmentId)
{

View File

@ -64,5 +64,4 @@ class ForgotPasswordController extends Controller
['email' => trans($response)]
);
}
}
}

View File

@ -70,7 +70,9 @@ class LoginController extends Controller
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) {
auth()->logout($user);
}
if (!$user->exists && $user->email === null && !$request->filled('email')) {
$request->flash();
@ -83,7 +85,6 @@ class LoginController extends Controller
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
@ -130,4 +131,4 @@ class LoginController extends Controller
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
}
}

View File

@ -91,6 +91,7 @@ class RegisterController extends Controller
/**
* Show the application registration form.
* @return Response
* @throws UserRegistrationException
*/
public function getRegister()
{
@ -102,20 +103,13 @@ class RegisterController extends Controller
/**
* Handle a registration request for the application.
* @param Request|\Illuminate\Http\Request $request
* @return Response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \Illuminate\Validation\ValidationException
*/
public function postRegister(Request $request)
{
$this->checkRegistrationAllowed();
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
$this->validator($request->all())->validate();
$userData = $request->all();
return $this->registerUser($userData);
@ -141,7 +135,6 @@ class RegisterController extends Controller
* @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws ConfirmationEmailException
*/
protected function registerUser(array $userData, $socialAccount = false)
{
@ -239,6 +232,8 @@ class RegisterController extends Controller
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
* @return mixed
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function socialRegister($socialDriver)
{
@ -272,8 +267,12 @@ class RegisterController extends Controller
}
$action = session()->pull('social-callback');
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
}
if ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
}
return redirect()->back();
}
@ -291,7 +290,6 @@ class RegisterController extends Controller
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws ConfirmationEmailException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
@ -308,5 +306,4 @@ class RegisterController extends Controller
];
return $this->registerUser($userData, $socialAccount);
}
}
}

View File

@ -46,4 +46,4 @@ class ResetPasswordController extends Controller
return redirect($this->redirectPath())
->with('status', trans($response));
}
}
}

View File

@ -36,17 +36,17 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 20);
$books = $this->entityRepo->getAllPaginated('book', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', 'list');
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$this->setPageTitle(trans('entities.books'));
return view('books/index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'new' => $new,
'booksViewType' => $booksViewType
]);
}
@ -109,7 +109,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
}
@ -155,7 +155,7 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false);
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
@ -190,42 +190,56 @@ class BookController extends Controller
}
// Sort pages and chapters
$sortedBooks = [];
$updatedModels = collect();
$sortMap = json_decode($request->get('sort-tree'));
$defaultBookId = $book->id;
$sortMap = collect(json_decode($request->get('sort-tree')));
$bookIdsInvolved = collect([$book->id]);
// Loop through contents of provided map and update entities accordingly
foreach ($sortMap as $bookChild) {
$priority = $bookChild->sort;
$id = intval($bookChild->id);
$isPage = $bookChild->type == 'page';
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
// Load models into map
$sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
$mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
$mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
// Store source and target books
$bookIdsInvolved->push(intval($mapItem->model->book_id));
$bookIdsInvolved->push(intval($mapItem->book));
});
// Update models only if there's a change in parent chain or ordering.
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
$model->priority = $priority;
if ($isPage) $model->chapter_id = $chapterId;
// Get the books involved in the sort
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
$booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get();
// Throw permission error if invalid ids or inaccessible books given.
if (count($bookIdsInvolved) !== count($booksInvolved)) {
$this->showPermissionError();
}
// Check permissions of involved books
$booksInvolved->each(function (Book $book) {
$this->checkOwnablePermission('book-update', $book);
});
// Perform the sort
$sortMap->each(function ($mapItem) {
$model = $mapItem->model;
$priorityChanged = intval($model->priority) !== intval($mapItem->sort);
$bookChanged = intval($model->book_id) !== intval($mapItem->book);
$chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
if ($bookChanged) {
$this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
}
if ($chapterChanged) {
$model->chapter_id = intval($mapItem->parentChapter);
$model->save();
$updatedModels->push($model);
}
// Store involved books to be sorted later
if (!in_array($bookId, $sortedBooks)) {
$sortedBooks[] = $bookId;
if ($priorityChanged) {
$model->priority = intval($mapItem->sort);
$model->save();
}
}
});
// Add activity for books
foreach ($sortedBooks as $bookId) {
/** @var Book $updatedBook */
$updatedBook = $this->entityRepo->getById('book', $bookId);
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Rebuild permissions and add activity for involved books.
$booksInvolved->each(function (Book $book) {
$this->entityRepo->buildJointPermissionsForBook($book);
Activity::add($book, 'book_sort', $book->id);
});
return redirect($book->getUrl());
}

View File

@ -159,7 +159,8 @@ class ChapterController extends Controller
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showMove($bookSlug, $chapterSlug) {
public function showMove($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
@ -177,7 +178,8 @@ class ChapterController extends Controller
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function move($bookSlug, $chapterSlug, Request $request) {
public function move($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);

View File

@ -51,7 +51,9 @@ abstract class Controller extends BaseController
*/
protected function preventAccessForDemoUsers()
{
if (config('app.env') === 'demo') $this->showPermissionError();
if (config('app.env') === 'demo') {
$this->showPermissionError();
}
}
/**
@ -100,7 +102,9 @@ abstract class Controller extends BaseController
*/
protected function checkOwnablePermission($permission, Ownable $ownable)
{
if (userCan($permission, $ownable)) return true;
if (userCan($permission, $ownable)) {
return true;
}
return $this->showPermissionError();
}
@ -113,7 +117,9 @@ abstract class Controller extends BaseController
protected function checkPermissionOr($permissionName, $callback)
{
$callbackResult = $callback();
if ($callbackResult === false) $this->checkPermission($permissionName);
if ($callbackResult === false) {
$this->checkPermission($permissionName);
}
return true;
}
@ -145,5 +151,4 @@ abstract class Controller extends BaseController
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
}

View File

@ -56,7 +56,8 @@ class HomeController extends Controller
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/
public function getTranslations() {
public function getTranslations()
{
$locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
@ -95,5 +96,4 @@ class HomeController extends Controller
{
return view('partials/custom-head-content');
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
@ -28,6 +29,21 @@ class ImageController extends Controller
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Get all images for a specific type, Paginated
* @param string $type
@ -47,14 +63,14 @@ class ImageController extends Controller
* @param Request $request
* @return mixed
*/
public function searchByType($type, $page = 0, Request $request)
public function searchByType(Request $request, $type, $page = 0)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
return response()->json($imgData);
}
@ -76,17 +92,19 @@ class ImageController extends Controller
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getGalleryFiltered($filter, $page = 0, Request $request)
public function getGalleryFiltered(Request $request, $filter, $page = 0)
{
$this->validate($request, [
'page_id' => 'required|integer'
]);
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
if (!$validFilters->contains($filter)) {
return response('Invalid filter', 500);
}
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
$imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
return response()->json($imgData);
}
@ -96,6 +114,7 @@ class ImageController extends Controller
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function uploadByType($type, Request $request)
{
@ -104,10 +123,14 @@ class ImageController extends Controller
'file' => 'is_image'
]);
if (!$this->imageRepo->isValidType($type)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$imageUpload = $request->file('file');
try {
$uploadedTo = $request->filled('uploaded_to') ? $request->get('uploaded_to') : 0;
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
@ -116,6 +139,73 @@ class ImageController extends Controller
return response()->json($image);
}
/**
* Upload a drawing to the system.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function uploadDrawing(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Replace the data content of a drawing.
* @param string $id
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function replaceDrawing(string $id, Request $request)
{
$this->validate($request, [
'image' => 'required|string'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-update', $image);
try {
$image = $this->imageRepo->replaceDrawingContent($image, $imageBase64Data);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getBase64Image($id)
{
$image = $this->imageRepo->getById($id);
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
/**
* Generate a sized thumbnail for an image.
* @param $id
@ -123,6 +213,8 @@ class ImageController extends Controller
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function getThumbnail($id, $width, $height, $crop)
{
@ -137,6 +229,8 @@ class ImageController extends Controller
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($imageId, Request $request)
{
@ -173,6 +267,4 @@ class ImageController extends Controller
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
}

View File

@ -145,6 +145,7 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return Response
* @throws NotFoundException
*/
public function show($bookSlug, $pageSlug)
{
@ -152,7 +153,9 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
} catch (NotFoundException $e) {
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
if ($page === null) {
throw $e;
}
return redirect($page->getUrl());
}
@ -219,7 +222,9 @@ class PageController extends Controller
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
if (count($warnings) > 0) {
session()->flash('warning', implode("\n", $warnings));
}
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
@ -603,5 +608,4 @@ class PageController extends Controller
session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}
}

View File

@ -67,7 +67,9 @@ class PermissionController extends Controller
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
return view('settings/roles/edit', ['role' => $role]);
}

View File

@ -104,7 +104,4 @@ class SearchController extends Controller
return view('search/entity-ajax-list', ['entities' => $entities]);
}
}

View File

@ -33,7 +33,9 @@ class SettingController extends Controller
// Cycles through posted settings and update them
foreach ($request->all() as $name => $value) {
if (strpos($name, 'setting-') !== 0) continue;
if (strpos($name, 'setting-') !== 0) {
continue;
}
$key = str_replace('setting-', '', trim($name));
Setting::put($key, $value);
}
@ -41,5 +43,4 @@ class SettingController extends Controller
session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings');
}
}

View File

@ -54,5 +54,4 @@ class TagController extends Controller
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
return response()->json($suggestions);
}
}

View File

@ -93,17 +93,7 @@ class UserController extends Controller
$user->roles()->sync($roles);
}
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
}
}
$this->userRepo->downloadGravatarToUserAvatar($user);
return redirect('/settings/users');
}
@ -249,4 +239,27 @@ class UserController extends Controller
'assetCounts' => $assetCounts
]);
}
/**
* Update the user's preferred book-list display setting.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function switchBookView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('book_view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'books_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
}
}

View File

@ -33,6 +33,14 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
],
'web_errors' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\BookStack\Http\Middleware\Localization::class
],
'api' => [
'throttle:60,1',
'bindings',

View File

@ -19,7 +19,9 @@ class Localization
$locale = $defaultLang;
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) continue;
if (!in_array($lang, $availableLocales)) {
continue;
}
$locale = $lang;
break;
}

View File

@ -15,5 +15,4 @@ class Model extends EloquentModel
{
return parent::getAttributeFromArray($key);
}
}
}

View File

@ -49,5 +49,4 @@ class ConfirmEmail extends Notification implements ShouldQueue
->line(trans('auth.email_confirm_text'))
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
abstract class Ownable extends Model
{
/**
@ -29,5 +28,4 @@ abstract class Ownable extends Model
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class Page extends Entity
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
@ -101,8 +100,8 @@ class Page extends Entity
* @return string
*/
public function entityRawQuery($withContent = false)
{ $htmlQuery = $withContent ? 'html' : "'' as html";
{
$htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
@ -31,7 +30,9 @@ class PageRevision extends Model
public function getUrl($path = null)
{
$url = $this->page->getUrl() . '/revisions/' . $this->id;
if ($path) return $url . '/' . trim($path, '/');
if ($path) {
return $url . '/' . trim($path, '/');
}
return $url;
}
@ -58,5 +59,4 @@ class PageRevision extends Model
{
return $type === 'revision';
}
}

View File

@ -15,12 +15,12 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
});
\Blade::directive('icon', function($expression) {
\Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";
});
@ -35,7 +35,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
$this->app->singleton(SettingService::class, function($app) {
$this->app->singleton(SettingService::class, function ($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
});
}

View File

@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider
*/
public function register()
{
Auth::provider('ldap', function($app, array $config) {
Auth::provider('ldap', function ($app, array $config) {
return new LdapUserProvider($config['model'], $app[LdapService::class]);
});
}

View File

@ -34,28 +34,28 @@ class CustomFacadeProvider extends ServiceProvider
*/
public function register()
{
$this->app->bind('activity', function() {
$this->app->bind('activity', function () {
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('views', function() {
$this->app->bind('views', function () {
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('setting', function() {
$this->app->bind('setting', function () {
return new SettingService(
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
});
$this->app->bind('images', function() {
$this->app->bind('images', function () {
return new ImageService(
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),

View File

@ -18,6 +18,8 @@ class EventServiceProvider extends ServiceProvider
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
'SocialiteProviders\Azure\AzureExtendSocialite@handle',
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
],
];

View File

@ -2,7 +2,6 @@
namespace BookStack\Providers;
use BookStack\Role;
use BookStack\Services\LdapService;
use BookStack\User;
@ -102,7 +101,9 @@ class LdapUserProvider implements UserProvider
{
// Get user via LDAP
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
if ($userDetails === null) return null;
if ($userDetails === null) {
return null;
}
// Search current user base by looking up a uid
$model = $this->createModel();
@ -110,7 +111,9 @@ class LdapUserProvider implements UserProvider
->where('external_auth_id', $userDetails['uid'])
->first();
if ($currentUser !== null) return $currentUser;
if ($currentUser !== null) {
return $currentUser;
}
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Providers;
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
use Illuminate\Pagination\Paginator;
@ -32,4 +31,4 @@ class PaginationServiceProvider extends IlluminatePaginationServiceProvider
return 1;
});
}
}
}

View File

@ -7,7 +7,8 @@ use BookStack\Entity;
* Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo {
class CommentRepo
{
/**
* @var Comment $comment
@ -39,7 +40,7 @@ class CommentRepo {
* @param array $data
* @return Comment
*/
public function create (Entity $entity, $data = [])
public function create(Entity $entity, $data = [])
{
$userId = user()->id;
$comment = $this->comment->newInstance($data);
@ -81,7 +82,9 @@ class CommentRepo {
protected function getNextLocalId(Entity $entity)
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) return 1;
if ($comments === null) {
return 1;
}
return $comments->local_id + 1;
}
}
}

View File

@ -77,11 +77,15 @@ class EntityRepo
* @param SearchService $searchService
*/
public function __construct(
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
ViewService $viewService, PermissionService $permissionService,
TagRepo $tagRepo, SearchService $searchService
)
{
Book $book,
Chapter $chapter,
Page $page,
PageRevision $pageRevision,
ViewService $viewService,
PermissionService $permissionService,
TagRepo $tagRepo,
SearchService $searchService
) {
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
@ -113,9 +117,9 @@ class EntityRepo
* @param bool $allowDrafts
* @return \Illuminate\Database\Query\Builder
*/
protected function entityQuery($type, $allowDrafts = false)
protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
{
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission);
if (strtolower($type) === 'page' && !$allowDrafts) {
$q = $q->where('draft', '=', false);
}
@ -163,14 +167,16 @@ class EntityRepo
$q = $this->entityQuery($type)->where('slug', '=', $slug);
if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
$q = $q->where('book_id', '=', function($query) use ($bookSlug) {
$q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
$query->select('id')
->from($this->book->getTable())
->where('slug', '=', $bookSlug)->limit(1);
});
}
$entity = $q->first();
if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
if ($entity === null) {
throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
}
return $entity;
}
@ -196,15 +202,18 @@ class EntityRepo
}
/**
* Get all entities of a type limited by count unless count if false.
* Get all entities of a type with the given permission, limited by count unless count is false.
* @param string $type
* @param integer|bool $count
* @param string $permission
* @return Collection
*/
public function getAll($type, $count = 20)
public function getAll($type, $count = 20, $permission = 'view')
{
$q = $this->entityQuery($type)->orderBy('name', 'asc');
if ($count !== false) $q = $q->take($count);
$q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
if ($count !== false) {
$q = $q->take($count);
}
return $q->get();
}
@ -231,7 +240,9 @@ class EntityRepo
{
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
->orderBy('created_at', 'desc');
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
if (strtolower($type) === 'page') {
$query = $query->where('draft', '=', false);
}
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
@ -250,7 +261,9 @@ class EntityRepo
{
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
->orderBy('updated_at', 'desc');
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
if (strtolower($type) === 'page') {
$query = $query->where('draft', '=', false);
}
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
@ -347,12 +360,16 @@ class EntityRepo
$parents[$key] = $entities[$index];
$parents[$key]->setAttribute('pages', collect());
}
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index];
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
$tree[] = $entities[$index];
}
$entities[$index]->book = $book;
}
foreach ($entities as $entity) {
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
continue;
}
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
if (!isset($parents[$parentKey])) {
$tree[] = $entity;
@ -431,7 +448,9 @@ class EntityRepo
if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
$query = $query->where('book_id', '=', $bookId);
}
if ($currentId) $query = $query->where('id', '!=', $currentId);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
@ -558,7 +577,9 @@ class EntityRepo
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
$slug = preg_replace('/\s{2,}/', ' ', $slug);
$slug = str_replace(' ', '-', $slug);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
if ($slug === "") {
$slug = substr(md5(rand(1, 500)), 0, 5);
}
return $slug;
}
@ -599,7 +620,9 @@ class EntityRepo
public function savePageRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->newInstance($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@ -627,7 +650,9 @@ class EntityRepo
*/
protected function formatHtml($htmlText)
{
if ($htmlText == '') return $htmlText;
if ($htmlText == '') {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
@ -641,7 +666,9 @@ class EntityRepo
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
if (get_class($childNode) !== 'DOMElement') {
continue;
}
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
@ -688,12 +715,17 @@ class EntityRepo
$content = $page->html;
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) return $content;
if (count($matches[0]) === 0) {
return $content;
}
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
$splitInclude = explode('#', $includeId, 2);
$pageId = intval($splitInclude[0]);
if (is_nan($pageId)) continue;
if (is_nan($pageId)) {
continue;
}
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
@ -714,8 +746,13 @@ class EntityRepo
continue;
}
$innerContent = '';
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
if ($isTopLevel) {
$innerContent .= $doc->saveHTML($matchingElem);
} else {
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
}
}
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
@ -748,7 +785,9 @@ class EntityRepo
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) $page->chapter_id = $chapter->id;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
@ -779,14 +818,18 @@ class EntityRepo
*/
public function getPageNav($pageContent)
{
if ($pageContent == '') return [];
if ($pageContent == '') {
return [];
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
if (is_null($headers)) return [];
if (is_null($headers)) {
return [];
}
$tree = collect([]);
foreach ($headers as $header) {
@ -802,7 +845,7 @@ class EntityRepo
// Normalise headers if only smaller headers have been used
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function($header) use ($minLevel) {
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
@ -838,7 +881,9 @@ class EntityRepo
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = $this->pageToPlainText($page);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->updated_by = $userId;
$page->revision_count++;
$page->save();
@ -900,7 +945,9 @@ class EntityRepo
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message;
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
return $message;
}
return $message . "\n" . trans('entities.pages_draft_edited_notification');
}
@ -996,7 +1043,9 @@ class EntityRepo
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
$draft->save();
return $draft;
@ -1103,17 +1152,4 @@ class EntityRepo
$page->delete();
}
}

View File

@ -1,12 +1,9 @@
<?php namespace BookStack\Repos;
use BookStack\Image;
use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo
@ -95,7 +92,7 @@ class ImageRepo
* @param string $searchTerm
* @return array
*/
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
@ -104,13 +101,13 @@ class ImageRepo
/**
* 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
* @param int $pageNum
* @param int $pageSize
* @return array
*/
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', 'gallery');
@ -123,7 +120,7 @@ class ImageRepo
$images = $images->whereIn('uploaded_to', $validPageIds);
}
return $this->returnPaginated($images, $pagination, $pageSize);
return $this->returnPaginated($images, $pageNum, $pageSize);
}
/**
@ -132,6 +129,8 @@ class ImageRepo
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{
@ -140,11 +139,39 @@ class ImageRepo
return $image;
}
/**
* Save a drawing the the database;
* @param string $base64Uri
* @param int $uploadedTo
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function saveDrawing(string $base64Uri, int $uploadedTo)
{
$name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png';
$image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
return $image;
}
/**
* Replace the image content of a drawing.
* @param Image $image
* @param string $base64Uri
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function replaceDrawingContent(Image $image, string $base64Uri)
{
return $this->imageService->replaceImageDataFromBase64Uri($image, $base64Uri);
}
/**
* Update the details of an image via an array of properties.
* @param Image $image
* @param array $updateDetails
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function updateImageDetails(Image $image, $updateDetails)
{
@ -170,6 +197,8 @@ class ImageRepo
/**
* Load thumbnails onto an image object.
* @param Image $image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
private function loadThumbs(Image $image)
{
@ -183,22 +212,46 @@ class ImageRepo
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* 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
* @return string
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
} catch (FileNotFoundException $exception) {
$image->delete();
return [];
} catch (\Exception $exception) {
dd($exception);
return null;
}
}
/**
* Get the raw image data from an Image.
* @param Image $image
* @return null|string
*/
public function getImageData(Image $image)
{
try {
return $this->imageService->getImageData($image);
} catch (\Exception $exception) {
return null;
}
}
}
/**
* Check if the provided image type is valid.
* @param $type
* @return bool
*/
public function isValidType($type)
{
$validTypes = ['drawing', 'gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Repos;
use BookStack\Exceptions\PermissionsException;
use BookStack\RolePermission;
use BookStack\Role;
@ -149,5 +148,4 @@ class PermissionsRepo
$this->permissionService->deleteJointPermissionsForRole($role);
$role->delete();
}
}
}

View File

@ -52,7 +52,9 @@ class TagRepo
public function getForEntity($entityType, $entityId)
{
$entity = $this->getEntity($entityType, $entityId);
if ($entity === null) return collect();
if ($entity === null) {
return collect();
}
return $entity->tags;
}
@ -95,7 +97,9 @@ class TagRepo
$query = $query->orderBy('count', 'desc')->take(50);
}
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
if ($tagName !== false) {
$query = $query->where('name', '=', $tagName);
}
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
return $query->get(['value'])->pluck('value');
@ -112,7 +116,9 @@ class TagRepo
$entity->tags()->delete();
$newTags = [];
foreach ($tags as $tag) {
if (trim($tag['name']) === '') continue;
if (trim($tag['name']) === '') {
continue;
}
$newTags[] = $this->newInstanceFromInput($tag);
}
@ -132,5 +138,4 @@ class TagRepo
$values = ['name' => $name, 'value' => $value];
return $this->tag->newInstance($values);
}
}
}

View File

@ -1,8 +1,12 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Image;
use BookStack\Role;
use BookStack\User;
use Exception;
use Images;
class UserRepo
{
@ -57,13 +61,13 @@ class UserRepo
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
$query->where(function($query) use ($term) {
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('email', 'like', $term);
});
@ -83,16 +87,7 @@ class UserRepo
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
$user->save();
\Log::error('Failed to save user gravatar image');
}
}
$this->downloadGravatarToUserAvatar($user);
return $user;
}
@ -104,10 +99,27 @@ class UserRepo
public function attachDefaultRole($user)
{
$roleId = setting('registration-role');
if ($roleId === false) $roleId = $this->role->first()->id;
if ($roleId === false) {
$roleId = $this->role->first()->id;
}
$user->attachRoleId($roleId);
}
/**
* Assign a user to a system-level role.
* @param User $user
* @param $systemRoleName
* @throws NotFoundException
*/
public function attachSystemRole(User $user, $systemRoleName)
{
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
if ($role === null) {
throw new NotFoundException("Role '{$systemRoleName}' not found");
}
$user->attachRole($role);
}
/**
* Checks if the give user is the only admin.
* @param User $user
@ -115,10 +127,14 @@ class UserRepo
*/
public function isOnlyAdmin(User $user)
{
if (!$user->roles->pluck('name')->contains('admin')) return false;
if (!$user->hasSystemRole('admin')) {
return false;
}
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
$adminRole = $this->role->getSystemRole('admin');
if ($adminRole->users->count() > 1) {
return false;
}
return true;
}
@ -140,11 +156,18 @@ class UserRepo
/**
* Remove the given user from storage, Delete all related content.
* @param User $user
* @throws Exception
*/
public function destroy(User $user)
{
$user->socialAccounts()->delete();
$user->delete();
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroyImage($image);
}
}
/**
@ -156,7 +179,7 @@ class UserRepo
*/
public function getActivity(User $user, $count = 20, $page = 0)
{
return \Activity::userActivity($user, $count, $page);
return Activity::userActivity($user, $count, $page);
}
/**
@ -213,4 +236,27 @@ class UserRepo
return $this->role->where('system_name', '!=', 'admin')->get();
}
}
/**
* Get a gravatar image for a user and set it as their avatar.
* Does not run if gravatar disabled in config.
* @param User $user
* @return bool
*/
public function downloadGravatarToUserAvatar(User $user)
{
// Get avatar from gravatar and save
if (!config('services.gravatar')) {
return false;
}
try {
$avatar = Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
return false;
}
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class Role extends Model
{
@ -40,7 +39,9 @@ class Role extends Model
{
$permissions = $this->getRelationValue('permissions');
foreach ($permissions as $permission) {
if ($permission->getRawAttribute('name') === $permissionName) return true;
if ($permission->getRawAttribute('name') === $permissionName) {
return true;
}
}
return false;
}
@ -91,5 +92,4 @@ class Role extends Model
{
return static::where('hidden', '=', false)->orderBy('name')->get();
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class RolePermission extends Model
{
/**
@ -8,7 +7,7 @@ class RolePermission extends Model
*/
public function roles()
{
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id');
}
/**

View File

@ -14,5 +14,4 @@ class SearchTerm extends Model
{
return $this->morphTo('entity');
}
}

View File

@ -170,5 +170,4 @@ class ActivityService
Session::flash('success', $message);
}
}
}
}

View File

@ -8,15 +8,37 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService extends UploadService
{
/**
* Get the storage that will be used for storing files.
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function getStorage()
{
if ($this->storageInstance !== null) {
return $this->storageInstance;
}
$storageType = config('filesystems.default');
// Override default location if set to local public to ensure not visible.
if ($storageType === 'local') {
$storageType = 'local_secure';
}
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
}
/**
* Get an attachment from storage.
* @param Attachment $attachment
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getAttachmentFromStorage(Attachment $attachment)
{
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
return $this->getStorage()->get($attachmentPath);
return $this->getStorage()->get($attachment->path);
}
/**
@ -92,16 +114,6 @@ class AttachmentService extends UploadService
]);
}
/**
* Get the file storage base path, amended for storage type.
* This allows us to keep a generic path in the database.
* @return string
*/
private function getStorageBasePath()
{
return $this->isLocal() ? 'storage/' : '';
}
/**
* Updates the file ordering for a listing of attached files.
* @param array $attachmentList
@ -138,6 +150,7 @@ class AttachmentService extends UploadService
/**
* Delete a File from the database and storage.
* @param Attachment $attachment
* @throws Exception
*/
public function deleteFile(Attachment $attachment)
{
@ -157,11 +170,10 @@ class AttachmentService extends UploadService
*/
protected function deleteFileInStorage(Attachment $attachment)
{
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
$storage = $this->getStorage();
$dirPath = dirname($storedFilePath);
$dirPath = dirname($attachment->path);
$storage->delete($storedFilePath);
$storage->delete($attachment->path);
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
@ -179,23 +191,20 @@ class AttachmentService extends UploadService
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
$uploadFileName = $attachmentName;
while ($storage->exists($storageBasePath . $uploadFileName)) {
while ($storage->exists($basePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}
$attachmentPath = $attachmentBasePath . $uploadFileName;
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
$attachmentPath = $basePath . $uploadFileName;
try {
$storage->put($attachmentStoragePath, $attachmentData);
$storage->put($attachmentPath, $attachmentData);
} catch (Exception $e) {
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
}
return $attachmentPath;
}
}
}

View File

@ -108,6 +108,4 @@ class EmailConfirmationService
}
return $token;
}
}
}

View File

@ -42,7 +42,7 @@ class ExportService
public function chapterToContainedHtml(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$pages->each(function ($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
@ -89,7 +89,7 @@ class ExportService
public function chapterToPdf(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$pages->each(function ($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
@ -163,7 +163,9 @@ class ExportService
$pathString = public_path(trim($relString, '/'));
}
if ($isLocal && !file_exists($pathString)) continue;
if ($isLocal && !file_exists($pathString)) {
continue;
}
try {
if ($isLocal) {
$imageContent = file_get_contents($pathString);
@ -173,7 +175,9 @@ class ExportService
$imageContent = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) throw new \Exception("Image fetch failed, Received error: " . $err);
if ($err) {
throw new \Exception("Image fetch failed, Received error: " . $err);
}
}
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
@ -257,17 +261,4 @@ class ExportService
}
return $text;
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Activity extends Facade
@ -10,5 +9,8 @@ class Activity extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'activity'; }
}
protected static function getFacadeAccessor()
{
return 'activity';
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Images extends Facade
@ -10,5 +9,8 @@ class Images extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'images'; }
}
protected static function getFacadeAccessor()
{
return 'images';
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Setting extends Facade
@ -10,5 +9,8 @@ class Setting extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'setting'; }
}
protected static function getFacadeAccessor()
{
return 'setting';
}
}

View File

@ -9,5 +9,8 @@ class Views extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'views'; }
}
protected static function getFacadeAccessor()
{
return 'views';
}
}

View File

@ -46,6 +46,49 @@ class ImageService extends UploadService
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
/**
* Save a new image from a uri-encoded base64 string of data.
* @param string $base64Uri
* @param string $name
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
throw new ImageUploadException("Invalid base64 image data provided");
}
$data = base64_decode($splitData[1]);
return $this->saveNew($name, $data, $type, $uploadedTo);
}
/**
* Replace the data for an image via a Base64 encoded string.
* @param Image $image
* @param string $base64Uri
* @return Image
* @throws ImageUploadException
*/
public function replaceImageDataFromBase64Uri(Image $image, string $base64Uri)
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
throw new ImageUploadException("Invalid base64 image data provided");
}
$data = base64_decode($splitData[1]);
$storage = $this->getStorage();
try {
$storage->put($image->path, $data);
} catch (Exception $e) {
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $image->path]));
}
return $image;
}
/**
* Gets an image from url and saves it to the database.
@ -59,7 +102,9 @@ class ImageService extends UploadService
{
$imageName = $imageName ? $imageName : basename($url);
$imageData = file_get_contents($url);
if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
if ($imageData === false) {
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}
return $this->saveNew($imageName, $imageData, $type);
}
@ -78,12 +123,12 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
if ($secureUploads) {
$imageName = str_random(16) . '-' . $imageName;
}
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
}
@ -96,8 +141,6 @@ class ImageService extends UploadService
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
}
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
$imageDetails = [
'name' => $imageName,
'path' => $fullPath,
@ -112,8 +155,8 @@ class ImageService extends UploadService
$imageDetails['updated_by'] = $userId;
}
$image = Image::forceCreate($imageDetails);
$image = (new Image());
$image->forceFill($imageDetails)->save();
return $image;
}
@ -124,14 +167,13 @@ class ImageService extends UploadService
*/
protected function getPath(Image $image)
{
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
return $image->path;
}
/**
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
* @param int $width
* @param int $height
@ -151,7 +193,6 @@ class ImageService extends UploadService
}
$storage = $this->getStorage();
if ($storage->exists($thumbFilePath)) {
return $this->getPublicUrl($thumbFilePath);
}
@ -161,9 +202,8 @@ class ImageService extends UploadService
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
} else {
throw $e;
}
throw $e;
}
if ($keepRatio) {
@ -183,10 +223,24 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath);
}
/**
* Get the raw data content from an image.
* @param Image $image
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getImageData(Image $image)
{
$imagePath = $this->getPath($image);
$storage = $this->getStorage();
return $storage->get($imagePath);
}
/**
* Destroys an Image object along with its files and thumbnails.
* @param Image $image
* @return bool
* @throws Exception
*/
public function destroyImage(Image $image)
{
@ -205,9 +259,13 @@ class ImageService extends UploadService
// Cleanup of empty folders
foreach ($storage->directories($imageFolder) as $directory) {
if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
if ($this->isFolderEmpty($directory)) {
$storage->deleteDirectory($directory);
}
}
if ($this->isFolderEmpty($imageFolder)) {
$storage->deleteDirectory($imageFolder);
}
if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
$image->delete();
return true;
@ -252,14 +310,10 @@ class ImageService extends UploadService
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
}
$this->storageUrl = $storageUrl;
}
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
$basePath = ($this->storageUrl == false) ? baseUrl('/') : $this->storageUrl;
return rtrim($basePath, '/') . $filePath;
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
@ -93,5 +92,4 @@ class Ldap
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
@ -45,7 +44,9 @@ class LdapService
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) return null;
if ($users['count'] === 0) {
return null;
}
$user = $users[0];
return [
@ -66,8 +67,12 @@ class LdapService
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;
if ($ldapUser === null) {
return false;
}
if ($ldapUser['uid'] !== $user->external_auth_id) {
return false;
}
$ldapConnection = $this->getConnection();
try {
@ -97,7 +102,9 @@ class LdapService
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
if (!$ldapBind) {
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
}
}
/**
@ -108,7 +115,9 @@ class LdapService
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) return $this->ldapConnection;
if ($this->ldapConnection !== null) {
return $this->ldapConnection;
}
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
@ -118,7 +127,9 @@ class LdapService
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) array_unshift($ldapServer, '');
if (!$hasProtocol) {
array_unshift($ldapServer, '');
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
@ -151,5 +162,4 @@ class LdapService
}
return strtr($filterString, $newAttrs);
}
}
}

View File

@ -88,7 +88,9 @@ class PermissionService
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if ($book === null) {
$book = false;
}
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
}
@ -108,7 +110,9 @@ class PermissionService
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if ($chapter === null) {
$chapter = false;
}
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
}
@ -122,7 +126,9 @@ class PermissionService
*/
protected function getRoles()
{
if ($this->userRoles !== false) return $this->userRoles;
if ($this->userRoles !== false) {
return $this->userRoles;
}
$roles = [];
@ -161,9 +167,9 @@ class PermissionService
*/
protected function bookFetchQuery()
{
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function($query) {
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
@ -174,7 +180,8 @@ class PermissionService
* @param array $roles
* @param bool $deleteOld
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
$entities = clone $books;
/** @var Book $book */
@ -187,7 +194,9 @@ class PermissionService
}
}
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities, $roles);
}
@ -261,7 +270,7 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
$roleIds = array_map(function($role) {
$roleIds = array_map(function ($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
@ -282,21 +291,22 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
if (count($entities) === 0) {
return;
}
$this->db->transaction(function() use ($entities) {
$this->db->transaction(function () use ($entities) {
foreach (array_chunk($entities, 1000) as $entityChunk) {
$query = $this->db->table('joint_permissions');
foreach ($entityChunk as $entity) {
$query->orWhere(function(QueryBuilder $query) use ($entity) {
$query->orWhere(function (QueryBuilder $query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
});
}
@ -315,7 +325,7 @@ class PermissionService
$permissionFetch = $this->entityPermission->newQuery();
foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function($query) use ($entity) {
$permissionFetch->orWhere(function ($query) use ($entity) {
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
});
}
@ -346,7 +356,7 @@ class PermissionService
}
}
$this->db->transaction(function() use ($jointPermissions) {
$this->db->transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
}
@ -362,8 +372,12 @@ class PermissionService
protected function getActions(Entity $entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
if ($entity->isA('chapter') || $entity->isA('book')) {
$baseActions[] = 'page-create';
}
if ($entity->isA('book')) {
$baseActions[] = 'chapter-create';
}
return $baseActions;
}
@ -412,7 +426,10 @@ class PermissionService
}
}
return $this->createJointPermissionDataArray($entity, $role, $action,
return $this->createJointPermissionDataArray(
$entity,
$role,
$action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
@ -426,7 +443,8 @@ class PermissionService
* @param $action
* @return bool
*/
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
{
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
return isset($entityMap[$key]) ? $entityMap[$key] : false;
}
@ -545,11 +563,12 @@ class PermissionService
* @param bool $fetchPageContent
* @return QueryBuilder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
{
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
@ -562,8 +581,8 @@ class PermissionService
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
});
@ -715,5 +734,4 @@ class PermissionService
$this->userRoles = false;
$this->isAdminUser = null;
}
}
}

View File

@ -83,7 +83,9 @@ class SearchService
$total = 0;
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) continue;
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
$total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
$results = $results->merge($search);
@ -111,7 +113,9 @@ class SearchService
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) continue;
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
@ -143,7 +147,9 @@ class SearchService
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType);
if ($getCount) return $query->count();
if ($getCount) {
return $query->count();
}
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
@ -164,12 +170,12 @@ class SearchService
if (count($terms['search']) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
$subQuery->where(function(Builder $query) use ($terms) {
$subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');
}
})->groupBy('entity_type', 'entity_id');
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
$join->on('id', '=', 'entity_id');
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
$entitySelect->mergeBindings($subQuery);
@ -177,7 +183,7 @@ class SearchService
// Handle exact term matching
if (count($terms['exact']) > 0) {
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
$entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
foreach ($terms['exact'] as $inputTerm) {
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%')
@ -195,7 +201,9 @@ class SearchService
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
$functionName = camel_case('filter_' . $filterTerm);
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
}
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
@ -234,7 +242,9 @@ class SearchService
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
if ($searchTerm !== '') {
$terms['search'][] = $searchTerm;
}
}
// Split filter values out
@ -267,15 +277,18 @@ class SearchService
* @param string $tagTerm
* @return mixed
*/
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm)
{
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
$query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
$tagName = $tagSplit[1];
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
$validOperator = in_array($tagOperator, $this->queryOperators);
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
if (!empty($tagName)) $query->where('name', '=', $tagName);
if (!empty($tagName)) {
$query->where('name', '=', $tagName);
}
if (is_numeric($tagValue) && $tagOperator !== 'like') {
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
// search the value as a string which prevents being able to do number-based operations
@ -323,7 +336,8 @@ class SearchService
* Index multiple Entities at once
* @param Entity[] $entities
*/
protected function indexEntities($entities) {
protected function indexEntities($entities)
{
$terms = [];
foreach ($entities as $entity) {
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
@ -386,7 +400,9 @@ class SearchService
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
if (!isset($tokenMap[$token])) {
$tokenMap[$token] = 0;
}
$tokenMap[$token]++;
$token = strtok($splitChars);
}
@ -410,43 +426,63 @@ class SearchService
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('updated_at', '>=', $date);
}
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('updated_at', '<', $date);
}
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('created_at', '>=', $date);
}
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('created_at', '<', $date);
}
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
if (!is_numeric($input) && $input !== 'me') {
return;
}
if ($input === 'me') {
$input = user()->id;
}
$query->where('created_by', '=', $input);
}
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
if (!is_numeric($input) && $input !== 'me') {
return;
}
if ($input === 'me') {
$input = user()->id;
}
$query->where('updated_by', '=', $input);
}
@ -455,7 +491,10 @@ class SearchService
$query->where('name', 'like', '%' .$input. '%');
}
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$this->filterInName($query, $model, $input);
}
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
@ -469,14 +508,14 @@ class SearchService
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->whereHas('views', function($query) {
$query->whereHas('views', function ($query) {
$query->where('user_id', '=', user()->id);
});
}
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->whereDoesntHave('views', function($query) {
$query->whereDoesntHave('views', function ($query) {
$query->where('user_id', '=', user()->id);
});
}
@ -484,7 +523,9 @@ class SearchService
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$functionName = camel_case('sort_by_' . $input);
if (method_exists($this, $functionName)) $this->$functionName($query, $model);
if (method_exists($this, $functionName)) {
$this->$functionName($query, $model);
}
}
@ -500,4 +541,4 @@ class SearchService
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
}
}
}

View File

@ -40,8 +40,12 @@ class SettingService
*/
public function get($key, $default = false)
{
if ($default === false) $default = config('setting-defaults.' . $key, false);
if (isset($this->localCache[$key])) return $this->localCache[$key];
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
$value = $this->getValueFromStore($key, $default);
$formatted = $this->formatValue($value, $default);
@ -72,12 +76,16 @@ class SettingService
{
// Check for an overriding value
$overrideValue = $this->getOverrideValue($key);
if ($overrideValue !== null) return $overrideValue;
if ($overrideValue !== null) {
return $overrideValue;
}
// Check the cache
$cacheKey = $this->cachePrefix . $key;
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) return $cacheVal;
if ($cacheVal !== null) {
return $cacheVal;
}
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
@ -98,6 +106,9 @@ class SettingService
{
$cacheKey = $this->cachePrefix . $key;
$this->cache->forget($cacheKey);
if (isset($this->localCache[$key])) {
unset($this->localCache[$key]);
}
}
/**
@ -109,11 +120,17 @@ class SettingService
protected function formatValue($value, $default)
{
// Change string booleans to actual booleans
if ($value === 'true') $value = true;
if ($value === 'false') $value = false;
if ($value === 'true') {
$value = true;
}
if ($value === 'false') {
$value = false;
}
// Set to default if empty
if ($value === '') $value = $default;
if ($value === '') {
$value = $default;
}
return $value;
}
@ -222,8 +239,9 @@ class SettingService
*/
protected function getOverrideValue($key)
{
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') {
return false;
}
return null;
}
}
}

View File

@ -16,7 +16,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch'];
/**
* SocialAuthService constructor.
@ -150,8 +150,12 @@ class SocialAuthService
{
$driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
if (!in_array($driver, $this->validSocialDrivers)) {
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driver)) {
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
}
return $driver;
}
@ -220,5 +224,4 @@ class SocialAuthService
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());
}
}
}

View File

@ -32,7 +32,9 @@ class UploadService
*/
protected function getStorage()
{
if ($this->storageInstance !== null) return $this->storageInstance;
if ($this->storageInstance !== null) {
return $this->storageInstance;
}
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
@ -40,7 +42,6 @@ class UploadService
return $this->storageInstance;
}
/**
* Check whether or not a folder is empty.
* @param $path
@ -61,4 +62,4 @@ class UploadService
{
return strtolower(config('filesystems.default')) === 'local';
}
}
}

View File

@ -27,7 +27,9 @@ class ViewService
public function add(Entity $entity)
{
$user = user();
if ($user === null || $user->isDefault()) return 0;
if ($user === null || $user->isDefault()) {
return 0;
}
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
@ -77,12 +79,16 @@ class ViewService
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
$user = user();
if ($user === null || $user->isDefault()) return collect();
if ($user === null || $user->isDefault()) {
return collect();
}
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
if ($filterModel) {
$query = $query->where('viewable_type', '=', get_class($filterModel));
}
$query = $query->where('user_id', '=', $user->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
@ -97,5 +103,4 @@ class ViewService
{
$this->view->truncate();
}
}
}

View File

@ -1,6 +1,5 @@
<?php namespace BookStack;
class SocialAccount extends Model
{

View File

@ -16,4 +16,4 @@ class Tag extends Model
{
return $this->morphTo('entity');
}
}
}

View File

@ -60,7 +60,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function roles()
{
if ($this->id === 0) return ;
if ($this->id === 0) {
return ;
}
return $this->belongsToMany(Role::class);
}
@ -81,7 +83,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function hasSystemRole($role)
{
return $this->roles->pluck('system_name')->contains('admin');
return $this->roles->pluck('system_name')->contains($role);
}
/**
@ -91,9 +93,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function permissions($cache = true)
{
if(isset($this->permissions) && $cache) return $this->permissions;
if (isset($this->permissions) && $cache) {
return $this->permissions;
}
$this->load('roles.permissions');
$permissions = $this->roles->map(function($role) {
$permissions = $this->roles->map(function ($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
@ -107,7 +111,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function can($permissionName)
{
if ($this->email === 'guest') return false;
if ($this->email === 'guest') {
return false;
}
return $this->permissions()->pluck('name')->contains($permissionName);
}
@ -162,7 +168,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
$default = baseUrl('/user_avatar.png');
$imageId = $this->image_id;
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
if ($imageId === 0 || $imageId === '0' || $imageId === null) {
return $default;
}
try {
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
@ -206,10 +214,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) return $this->name;
if (strlen($this->name) <= $chars) {
return $this->name;
}
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) return $splitName[0];
if (strlen($splitName[0]) <= $chars) {
return $splitName[0];
}
return '';
}

View File

@ -74,7 +74,9 @@ function userCan($permission, Ownable $ownable = null)
function setting($key = null, $default = false)
{
$settingService = resolve(\BookStack\Services\SettingService::class);
if (is_null($key)) return $settingService;
if (is_null($key)) {
return $settingService;
}
return $settingService->get($key, $default);
}
@ -87,7 +89,9 @@ function setting($key = null, $default = false)
function baseUrl($path, $forceAppDomain = false)
{
$isFullUrl = strpos($path, 'http') === 0;
if ($isFullUrl && !$forceAppDomain) return $path;
if ($isFullUrl && !$forceAppDomain) {
return $path;
}
$path = trim($path, '/');
// Remove non-specified domain if forced and we have a domain
@ -126,7 +130,8 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null)
return app('redirect')->to($to, $status, $headers, $secure);
}
function icon($name, $attrs = []) {
function icon($name, $attrs = [])
{
$iconPath = resource_path('assets/icons/' . $name . '.svg');
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
@ -159,11 +164,15 @@ function sortUrl($path, $data, $overrideData = [])
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') continue;
if ($trimmedVal === '') {
continue;
}
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
if (count($queryStringSections) === 0) return $path;
if (count($queryStringSections) === 0) {
return $path;
}
return baseUrl($path . '?' . implode('&', $queryStringSections));
}
}

View File

@ -18,7 +18,9 @@
"barryvdh/laravel-snappy": "^0.4.0",
"socialiteproviders/slack": "^3.0",
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0"
"socialiteproviders/okta": "^1.0",
"socialiteproviders/gitlab": "^3.0",
"socialiteproviders/twitch": "^3.0"
},
"require-dev": {
"filp/whoops": "~2.0",
@ -29,7 +31,8 @@
"symfony/dom-crawler": "3.1.*",
"laravel/browser-kit-testing": "^2.0",
"barryvdh/laravel-ide-helper": "^2.4.1",
"barryvdh/laravel-debugbar": "^3.1.0"
"barryvdh/laravel-debugbar": "^3.1.0",
"squizlabs/php_codesniffer": "^3.2"
},
"autoload": {
"classmap": [

379
composer.lock generated
View File

@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "7d60f09393b99551e9ffdb6622ed7ade",
"content-hash": "ed85d10e69b1071020178cb400a80e48",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.45.3",
"version": "3.52.6",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "d0abb0b1194fa64973b135191f56df991bc5787c"
"reference": "c9af7657eddc0267cc7ac4f969c10d5c18459992"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d0abb0b1194fa64973b135191f56df991bc5787c",
"reference": "d0abb0b1194fa64973b135191f56df991bc5787c",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c9af7657eddc0267cc7ac4f969c10d5c18459992",
"reference": "c9af7657eddc0267cc7ac4f969c10d5c18459992",
"shasum": ""
},
"require": {
@ -84,25 +84,25 @@
"s3",
"sdk"
],
"time": "2017-12-08T21:36:50+00:00"
"time": "2018-02-09T22:53:37+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
"version": "v0.8.1",
"version": "v0.8.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
"reference": "3b2235e589616331d68482d61b7763789a2600fe"
"reference": "7dcdecfa125c174d0abe723603633dc2756ea3af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/3b2235e589616331d68482d61b7763789a2600fe",
"reference": "3b2235e589616331d68482d61b7763789a2600fe",
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7dcdecfa125c174d0abe723603633dc2756ea3af",
"reference": "7dcdecfa125c174d0abe723603633dc2756ea3af",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^0.8",
"illuminate/support": "5.1.x|5.2.x|5.3.x|5.4.x|5.5.x",
"illuminate/support": "5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x",
"php": ">=5.5.9"
},
"type": "library",
@ -140,32 +140,32 @@
"laravel",
"pdf"
],
"time": "2017-07-29T19:01:17+00:00"
"time": "2018-02-07T17:43:25+00:00"
},
{
"name": "barryvdh/laravel-snappy",
"version": "v0.4.0",
"version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-snappy.git",
"reference": "f08c7e5b4ddea585bfcd48ab4f40f920e58dd1cf"
"reference": "5f6e7f3ba15c867d1b8e2885d454110270616ebe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/f08c7e5b4ddea585bfcd48ab4f40f920e58dd1cf",
"reference": "f08c7e5b4ddea585bfcd48ab4f40f920e58dd1cf",
"url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/5f6e7f3ba15c867d1b8e2885d454110270616ebe",
"reference": "5f6e7f3ba15c867d1b8e2885d454110270616ebe",
"shasum": ""
},
"require": {
"illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x",
"illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x",
"knplabs/knp-snappy": "*",
"illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x",
"illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x",
"knplabs/knp-snappy": "^1",
"php": ">=5.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.3-dev"
"dev-master": "0.4-dev"
},
"laravel": {
"providers": [
@ -201,7 +201,7 @@
"wkhtmltoimage",
"wkhtmltopdf"
],
"time": "2017-08-14T06:48:50+00:00"
"time": "2018-02-08T15:58:26+00:00"
},
{
"name": "cogpowered/finediff",
@ -898,16 +898,16 @@
},
{
"name": "knplabs/knp-snappy",
"version": "v1.0.3",
"version": "v1.0.4",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/snappy.git",
"reference": "68590ef3aa94425b1c0019cc28ce471729f51fcb"
"reference": "144c4ecd1ccaeda936bf832b93079efc490e6850"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/KnpLabs/snappy/zipball/68590ef3aa94425b1c0019cc28ce471729f51fcb",
"reference": "68590ef3aa94425b1c0019cc28ce471729f51fcb",
"url": "https://api.github.com/repos/KnpLabs/snappy/zipball/144c4ecd1ccaeda936bf832b93079efc490e6850",
"reference": "144c4ecd1ccaeda936bf832b93079efc490e6850",
"shasum": ""
},
"require": {
@ -960,20 +960,20 @@
"thumbnail",
"wkhtmltopdf"
],
"time": "2017-12-03T23:18:18+00:00"
"time": "2018-01-22T19:40:51+00:00"
},
{
"name": "laravel/framework",
"version": "v5.5.24",
"version": "v5.5.34",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "06135405bb1f736dac5e9529ed1541fc446c9c0f"
"reference": "1de7c0aec13eadbdddc2d1ba4019b064b2c6b966"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/06135405bb1f736dac5e9529ed1541fc446c9c0f",
"reference": "06135405bb1f736dac5e9529ed1541fc446c9c0f",
"url": "https://api.github.com/repos/laravel/framework/zipball/1de7c0aec13eadbdddc2d1ba4019b064b2c6b966",
"reference": "1de7c0aec13eadbdddc2d1ba4019b064b2c6b966",
"shasum": ""
},
"require": {
@ -1053,6 +1053,7 @@
"guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).",
"laravel/tinker": "Required to use the tinker console command (~1.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
"league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).",
"league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
"nexmo/client": "Required to use the Nexmo transport (~1.0).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).",
@ -1093,7 +1094,7 @@
"framework",
"laravel"
],
"time": "2017-12-07T01:28:21+00:00"
"time": "2018-02-06T15:36:55+00:00"
},
{
"name": "laravel/socialite",
@ -1159,16 +1160,16 @@
},
{
"name": "league/flysystem",
"version": "1.0.41",
"version": "1.0.42",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "f400aa98912c561ba625ea4065031b7a41e5a155"
"reference": "09eabc54e199950041aef258a85847676496fe8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f400aa98912c561ba625ea4065031b7a41e5a155",
"reference": "f400aa98912c561ba625ea4065031b7a41e5a155",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/09eabc54e199950041aef258a85847676496fe8e",
"reference": "09eabc54e199950041aef258a85847676496fe8e",
"shasum": ""
},
"require": {
@ -1179,12 +1180,13 @@
},
"require-dev": {
"ext-fileinfo": "*",
"mockery/mockery": "~0.9",
"phpspec/phpspec": "^2.2",
"phpunit/phpunit": "~4.8"
"phpspec/phpspec": "^3.4",
"phpunit/phpunit": "^5.7"
},
"suggest": {
"ext-fileinfo": "Required for MimeType",
"ext-ftp": "Allows you to use FTP server storage",
"ext-openssl": "Allows you to use FTPS server storage",
"league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
"league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
@ -1238,7 +1240,7 @@
"sftp",
"storage"
],
"time": "2017-08-06T17:41:04+00:00"
"time": "2018-01-27T16:03:56+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
@ -1951,16 +1953,16 @@
},
{
"name": "ramsey/uuid",
"version": "3.7.1",
"version": "3.7.3",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "45cffe822057a09e05f7bd09ec5fb88eeecd2334"
"reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/45cffe822057a09e05f7bd09ec5fb88eeecd2334",
"reference": "45cffe822057a09e05f7bd09ec5fb88eeecd2334",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/44abcdad877d9a46685a3a4d221e3b2c4b87cb76",
"reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76",
"shasum": ""
},
"require": {
@ -1971,17 +1973,15 @@
"rhumsaa/uuid": "self.version"
},
"require-dev": {
"apigen/apigen": "^4.1",
"codeception/aspect-mock": "^1.0 | ^2.0",
"codeception/aspect-mock": "^1.0 | ~2.0.0",
"doctrine/annotations": "~1.2.0",
"goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1",
"ircmaxell/random-lib": "^1.1",
"jakub-onderka/php-parallel-lint": "^0.9.0",
"mockery/mockery": "^0.9.4",
"mockery/mockery": "^0.9.9",
"moontoast/math": "^1.1",
"php-mock/php-mock-phpunit": "^0.3|^1.1",
"phpunit/phpunit": "^4.7|>=5.0 <5.4",
"satooshi/php-coveralls": "^0.6.1",
"phpunit/phpunit": "^4.7|^5.0",
"squizlabs/php_codesniffer": "^2.3"
},
"suggest": {
@ -2029,7 +2029,7 @@
"identifier",
"uuid"
],
"time": "2017-09-22T20:46:04+00:00"
"time": "2018-01-20T00:28:24+00:00"
},
{
"name": "sabberworm/php-css-parser",
@ -2076,17 +2076,54 @@
"time": "2016-07-19T19:14:21+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.0",
"name": "socialiteproviders/gitlab",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "ac108bce073135a55dfebf28ceaf1459669348e8"
"url": "https://github.com/SocialiteProviders/GitLab.git",
"reference": "c96dc004563a3caf157608fe9aa9e45c79065d00"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/ac108bce073135a55dfebf28ceaf1459669348e8",
"reference": "ac108bce073135a55dfebf28ceaf1459669348e8",
"url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/c96dc004563a3caf157608fe9aa9e45c79065d00",
"reference": "c96dc004563a3caf157608fe9aa9e45c79065d00",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
"socialiteproviders/manager": "~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\GitLab\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christoffer Martinsen",
"email": "christoffermartinsen@gmail.com"
}
],
"description": "GitLab OAuth2 Provider for Laravel Socialite",
"time": "2017-01-31T05:06:13+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"shasum": ""
},
"require": {
@ -2122,7 +2159,7 @@
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"time": "2017-09-21T07:21:55+00:00"
"time": "2017-11-20T08:42:57+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@ -2235,6 +2272,43 @@
"description": "Slack OAuth2 Provider for Laravel Socialite",
"time": "2017-04-10T05:10:48+00:00"
},
{
"name": "socialiteproviders/twitch",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Twitch.git",
"reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
"reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
"socialiteproviders/manager": "~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Twitch\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Faust",
"email": "hello@brianfaust.de"
}
],
"description": "Twitch OAuth2 Provider for Laravel Socialite",
"time": "2017-01-25T09:48:29+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.0.2",
@ -2721,16 +2795,16 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.6.0",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
"reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"shasum": ""
},
"require": {
@ -2742,7 +2816,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6-dev"
"dev-master": "1.7-dev"
}
},
"autoload": {
@ -2776,7 +2850,7 @@
"portable",
"shim"
],
"time": "2017-10-11T12:05:26+00:00"
"time": "2018-01-30T19:27:44+00:00"
},
{
"name": "symfony/process",
@ -3040,29 +3114,29 @@
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.0",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
"reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b"
"reference": "0ed4a2ea4e0902dac0489e6436ebcd5bbcae9757"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b",
"reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0ed4a2ea4e0902dac0489e6436ebcd5bbcae9757",
"reference": "0ed4a2ea4e0902dac0489e6436ebcd5bbcae9757",
"shasum": ""
},
"require": {
"php": "^5.5 || ^7",
"symfony/css-selector": "^2.7|~3.0"
"php": "^5.5 || ^7.0",
"symfony/css-selector": "^2.7 || ^3.0 || ^4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.8|5.1.*"
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
"dev-master": "2.2.x-dev"
}
},
"autoload": {
@ -3083,7 +3157,7 @@
],
"description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
"time": "2016-09-20T12:50:39+00:00"
"time": "2017-11-27T11:13:29+00:00"
},
{
"name": "vlucas/phpdotenv",
@ -3139,26 +3213,26 @@
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "01a859752094e00aa8548832312366753272f8af"
"reference": "f0018d359a2ad6968ad11b283283a925e017f3c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/01a859752094e00aa8548832312366753272f8af",
"reference": "01a859752094e00aa8548832312366753272f8af",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f0018d359a2ad6968ad11b283283a925e017f3c9",
"reference": "f0018d359a2ad6968ad11b283283a925e017f3c9",
"shasum": ""
},
"require": {
"illuminate/routing": "5.5.x",
"illuminate/session": "5.5.x",
"illuminate/support": "5.5.x",
"maximebf/debugbar": "~1.14.0",
"illuminate/routing": "5.5.x|5.6.x",
"illuminate/session": "5.5.x|5.6.x",
"illuminate/support": "5.5.x|5.6.x",
"maximebf/debugbar": "~1.15.0",
"php": ">=7.0",
"symfony/debug": "^3",
"symfony/finder": "^3"
"symfony/debug": "^3|^4",
"symfony/finder": "^3|^4"
},
"require-dev": {
"illuminate/framework": "5.5.x"
@ -3166,7 +3240,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.2-dev"
},
"laravel": {
"providers": [
@ -3203,34 +3277,34 @@
"profiler",
"webprofiler"
],
"time": "2017-09-18T13:32:46+00:00"
"time": "2018-02-07T08:29:09+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
"version": "v2.4.1",
"version": "v2.4.3",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8"
"reference": "5c304db44fba8e9c4aa0c09739e59f7be7736fdd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/2b1273c45e2f8df7a625563e2283a17c14f02ae8",
"reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/5c304db44fba8e9c4aa0c09739e59f7be7736fdd",
"reference": "5c304db44fba8e9c4aa0c09739e59f7be7736fdd",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.0.4",
"illuminate/console": "^5.0,<5.6",
"illuminate/filesystem": "^5.0,<5.6",
"illuminate/support": "^5.0,<5.6",
"illuminate/console": "^5.0,<5.7",
"illuminate/filesystem": "^5.0,<5.7",
"illuminate/support": "^5.0,<5.7",
"php": ">=5.4.0",
"symfony/class-loader": "^2.3|^3.0"
},
"require-dev": {
"doctrine/dbal": "~2.3",
"illuminate/config": "^5.0,<5.6",
"illuminate/view": "^5.0,<5.6",
"illuminate/config": "^5.0,<5.7",
"illuminate/view": "^5.0,<5.7",
"phpunit/phpunit": "4.*",
"scrutinizer/ocular": "~1.1",
"squizlabs/php_codesniffer": "~2.3"
@ -3276,7 +3350,7 @@
"phpstorm",
"sublime"
],
"time": "2017-07-16T00:24:12+00:00"
"time": "2018-02-08T07:56:07+00:00"
},
{
"name": "barryvdh/reflection-docblock",
@ -3590,22 +3664,22 @@
},
{
"name": "maximebf/debugbar",
"version": "v1.14.1",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
"reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2"
"reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/64251a392344e3d22f3d21c3b7c531ba96eb01d2",
"reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30e7d60937ee5f1320975ca9bc7bcdd44d500f07",
"reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/log": "^1.0",
"symfony/var-dumper": "^2.6|^3.0"
"symfony/var-dumper": "^2.6|^3.0|^4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0|^5.0"
@ -3647,7 +3721,7 @@
"debug",
"debugbar"
],
"time": "2017-09-13T12:19:36+00:00"
"time": "2017-12-15T11:13:46+00:00"
},
{
"name": "mockery/mockery",
@ -3917,16 +3991,16 @@
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "4.2.0",
"version": "4.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "66465776cfc249844bde6d117abff1d22e06c2da"
"reference": "94fd0001232e47129dd3504189fa1c7225010d08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da",
"reference": "66465776cfc249844bde6d117abff1d22e06c2da",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
"reference": "94fd0001232e47129dd3504189fa1c7225010d08",
"shasum": ""
},
"require": {
@ -3964,7 +4038,7 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2017-11-27T17:38:31+00:00"
"time": "2017-11-30T07:14:17+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@ -4327,16 +4401,16 @@
},
{
"name": "phpunit/phpunit",
"version": "6.5.3",
"version": "6.5.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "882e886cc928a0abd3c61282b2a64026237d14a4"
"reference": "3330ef26ade05359d006041316ed0fa9e8e3cefe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/882e886cc928a0abd3c61282b2a64026237d14a4",
"reference": "882e886cc928a0abd3c61282b2a64026237d14a4",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3330ef26ade05359d006041316ed0fa9e8e3cefe",
"reference": "3330ef26ade05359d006041316ed0fa9e8e3cefe",
"shasum": ""
},
"require": {
@ -4354,7 +4428,7 @@
"phpunit/php-file-iterator": "^1.4.3",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-timer": "^1.0.9",
"phpunit/phpunit-mock-objects": "^5.0.4",
"phpunit/phpunit-mock-objects": "^5.0.5",
"sebastian/comparator": "^2.1",
"sebastian/diff": "^2.0",
"sebastian/environment": "^3.1",
@ -4407,27 +4481,27 @@
"testing",
"xunit"
],
"time": "2017-12-06T09:42:03+00:00"
"time": "2018-02-01T05:57:37+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "5.0.4",
"version": "5.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "16b50f4167e5e85e81ca8a3dd105d0a5fd32009a"
"reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/16b50f4167e5e85e81ca8a3dd105d0a5fd32009a",
"reference": "16b50f4167e5e85e81ca8a3dd105d0a5fd32009a",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
"reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.5",
"php": "^7.0",
"phpunit/php-text-template": "^1.2.1",
"sebastian/exporter": "^3.0"
"sebastian/exporter": "^3.1"
},
"conflict": {
"phpunit/phpunit": "<6.0"
@ -4466,7 +4540,7 @@
"mock",
"xunit"
],
"time": "2017-12-02T05:31:19+00:00"
"time": "2018-01-06T05:45:45+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@ -4515,21 +4589,21 @@
},
{
"name": "sebastian/comparator",
"version": "2.1.0",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "1174d9018191e93cb9d719edec01257fc05f8158"
"reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1174d9018191e93cb9d719edec01257fc05f8158",
"reference": "1174d9018191e93cb9d719edec01257fc05f8158",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
"reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
"shasum": ""
},
"require": {
"php": "^7.0",
"sebastian/diff": "^2.0",
"sebastian/diff": "^2.0 || ^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
@ -4575,7 +4649,7 @@
"compare",
"equality"
],
"time": "2017-11-03T07:16:52+00:00"
"time": "2018-02-01T13:46:46+00:00"
},
{
"name": "sebastian/diff",
@ -5027,6 +5101,57 @@
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2016-10-03T07:35:21+00:00"
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
"reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
},
"bin": [
"bin/phpcs",
"bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "http://www.squizlabs.com/php-codesniffer",
"keywords": [
"phpcs",
"standards"
],
"time": "2017-12-19T21:44:46+00:00"
},
{
"name": "symfony/class-loader",
"version": "v3.3.6",
@ -5181,16 +5306,16 @@
},
{
"name": "webmozart/assert",
"version": "1.2.0",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f"
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f",
"reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"shasum": ""
},
"require": {
@ -5227,7 +5352,7 @@
"check",
"validate"
],
"time": "2016-11-23T20:04:58+00:00"
"time": "2018-01-29T19:49:41+00:00"
}
],
"aliases": [],

View File

@ -2,11 +2,14 @@
return [
'env' => env('APP_ENV', 'production'),
'editor' => env('APP_EDITOR', 'html'),
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list')
],
/*
|--------------------------------------------------------------------------
| Application Debug Mode
@ -58,7 +61,7 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it', 'ru'],
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN'],
/*
|--------------------------------------------------------------------------

View File

@ -56,7 +56,12 @@ return [
'local' => [
'driver' => 'local',
'root' => base_path(),
'root' => public_path(),
],
'local_secure' => [
'driver' => 'local',
'root' => storage_path(),
],
'ftp' => [

View File

@ -13,7 +13,13 @@ return [
| to have a conventional place to find your various credentials.
|
*/
// Single option to disable non-auth external services such as Gravatar and Draw.io
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
'gravatar' => env('GRAVATAR', !env('DISABLE_EXTERNAL_SERVICES', false)),
'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)),
'callback_url' => env('APP_URL', false),
'mailgun' => [
@ -86,7 +92,22 @@ return [
'redirect' => env('APP_URL') . '/login/service/okta/callback',
'base_url' => env('OKTA_BASE_URL'),
'name' => 'Okta',
],
],
'gitlab' => [
'client_id' => env('GITLAB_APP_ID'),
'client_secret' => env('GITLAB_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/gitlab/callback',
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
'name' => 'GitLab',
],
'twitch' => [
'client_id' => env('TWITCH_APP_ID'),
'client_secret' => env('TWITCH_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
'name' => 'Twitch',
],
'ldap' => [
'server' => env('LDAP_SERVER', false),

View File

@ -1,5 +1,10 @@
<?php
$viewPaths = [realpath(base_path('resources/views'))];
if ($theme = env('APP_THEME', false)) {
array_unshift($viewPaths, base_path('themes/' . $theme));
}
return [
/*
@ -13,9 +18,7 @@ return [
|
*/
'paths' => [
realpath(base_path('resources/views')),
],
'paths' => $viewPaths,
/*
|--------------------------------------------------------------------------

View File

@ -11,7 +11,7 @@
"babelify": "^7.3.0",
"browserify": "^14.3.0",
"envify": "^4.0.0",
"gulp": "3.9.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "3.1.1",
"gulp-clean-css": "^3.0.4",
"gulp-livereload": "^3.8.1",

8
phpcs.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<ruleset name="PHP_CodeSniffer">
<description>The coding standard for BookStack.</description>
<file>app</file>
<exclude-pattern>*/migrations/*</exclude-pattern>
<arg value="np"/>
<rule ref="PSR2"/>
</ruleset>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="410.44821"
height="419.86591"
viewBox="0 0 108.59775 111.08952"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="drawing.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="314.26392"
inkscape:cy="340.27949"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2560"
inkscape:window-height="1386"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-14.87623,-172.69189)">
<path
style="opacity:1;fill:#000016;fill-opacity:1;stroke:#000000;stroke-width:2.76340532;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 86.878856,250.68964 -11.880088,9.48754 11.880088,9.48722 z"
id="rect872"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path815"
cx="36.348648"
cy="196.87526"
r="18.972418" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect817"
width="44.366741"
height="44.366741"
x="77.107246"
y="174.69189" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3.96875;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect836"
width="44.366741"
height="44.366741"
x="17.773417"
y="237.4303" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3.96875;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect838"
width="19.777945"
height="0.13363476"
x="56.260235"
y="196.77391" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.0334897;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect838-6"
width="39.624786"
height="0.068895064"
x="220.46501"
y="-99.424637"
transform="rotate(90)" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.02711964;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect838-6-7"
width="36.156651"
height="0.075265162"
x="-99.381981"
y="-260.21466"
transform="scale(-1)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -72,7 +72,17 @@ Some strings have colon-prefixed variables in such as `:userName`. Leave these v
Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
Pull requests are very welcome. If the scope of your pull request is very large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
### Standards
PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
### Pull Requests
Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
## Website, Docs & Blog
@ -105,3 +115,4 @@ These are the great open-source projects used to help build BookStack:
* [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy)
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
* [Draw.io](https://github.com/jgraph/drawio)

View File

@ -0,0 +1 @@
<svg viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g id="Group"><path id="Fill-4" d="M32.134,64.345l11.785,-39.372l-23.569,0l11.784,39.372Z" style="fill:#e24329;"/><path id="Fill-8" d="M32.134,64.345l-11.784,-39.372l-16.515,0l28.299,39.372Z" style="fill:#fc6d26;"/><path id="Fill-12" d="M3.835,24.973l-3.581,11.965c-0.327,1.091 0.031,2.287 0.886,2.961l30.994,24.446l-28.299,-39.372Z" style="fill:#fca326;"/><path id="Fill-16" d="M3.835,24.973l16.515,0l-7.097,-23.713c-0.366,-1.22 -1.956,-1.22 -2.321,0l-7.097,23.713Z" style="fill:#e24329;"/><path id="Fill-18" d="M32.134,64.345l11.785,-39.372l16.515,0l-28.3,39.372Z" style="fill:#fc6d26;"/><path id="Fill-20" d="M60.434,24.973l3.581,11.965c0.326,1.091 -0.031,2.287 -0.886,2.961l-30.995,24.446l28.3,-39.372Z" style="fill:#fca326;"/><path id="Fill-22" d="M60.434,24.973l-16.515,0l7.097,-23.713c0.365,-1.22 1.955,-1.22 2.32,0l7.098,23.713Z" style="fill:#e24329;"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 134"><defs><style>.cls-1{fill:#6441a4;fill-rule:evenodd;}</style></defs><title>Glitch</title><path fill="#6441a4" class="cls-1" d="M89,77l-9,23v94h32v17h18l17-17h26l35-35V77H89Zm107,76-20,20H144l-17,17V173H100V89h96v64Zm-20-41v35H164V112h12Zm-32,0v35H132V112h12Z" transform="translate(-80 -77)"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@ -1,6 +1,8 @@
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('../code');
const code = require('../libs/code');
const DrawIO = require('../libs/drawio');
class MarkdownEditor {
@ -20,13 +22,26 @@ class MarkdownEditor {
init() {
let lastClick = 0;
// Prevent markdown display link click redirect
this.display.addEventListener('click', event => {
let link = event.target.closest('a');
if (link === null) return;
let isDblClick = Date.now() - lastClick < 300;
event.preventDefault();
window.open(link.getAttribute('href'));
let link = event.target.closest('a');
if (link !== null) {
event.preventDefault();
window.open(link.getAttribute('href'));
return;
}
let drawing = event.target.closest('[drawio-diagram]');
if (drawing !== null && isDblClick) {
this.actionEditDrawing(drawing);
return;
}
lastClick = Date.now();
});
// Button actions
@ -37,6 +52,7 @@ class MarkdownEditor {
let action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
if (action === 'insertDrawing') this.actionStartDrawing();
});
window.$events.listen('editor-markdown-update', value => {
@ -290,6 +306,70 @@ class MarkdownEditor {
});
}
// Show draw.io if enabled and handle save.
actionStartDrawing() {
if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
let cursorPos = this.cm.getCursor('from');
DrawIO.show(() => {
return Promise.resolve('');
}, (pngData) => {
// let id = "image-" + Math.random().toString(16).slice(2);
// let loadingImage = window.baseUrl('/loading.gif');
let data = {
image: pngData,
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => {
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
DrawIO.close();
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
});
});
}
// Show draw.io if enabled and handle save.
actionEditDrawing(imgContainer) {
if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
let cursorPos = this.cm.getCursor('from');
let drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(() => {
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
return `data:image/png;base64,${resp.data.content}`;
});
}, (pngData) => {
let data = {
image: pngData,
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => {
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url + `?updated=${Date.now()}`}"></div>`;
let newContent = this.cm.getValue().split('\n').map(line => {
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
return newText;
}
return line;
}).join('\n');
this.cm.setValue(newContent);
this.cm.setCursor(cursorPos);
this.cm.focus();
DrawIO.close();
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
});
});
}
}
module.exports = MarkdownEditor ;

View File

@ -0,0 +1,69 @@
const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json';
let iFrame = null;
let onInit, onSave;
/**
* Show the draw.io editor.
* @param onInitCallback - Must return a promise with the xml to load for the editor.
* @param onSaveCallback - Is called with the drawing data on save.
*/
function show(onInitCallback, onSaveCallback) {
onInit = onInitCallback;
onSave = onSaveCallback;
iFrame = document.createElement('iframe');
iFrame.setAttribute('frameborder', '0');
window.addEventListener('message', drawReceive);
iFrame.setAttribute('src', drawIoUrl);
iFrame.setAttribute('class', 'fullscreen');
iFrame.style.backgroundColor = '#FFFFFF';
document.body.appendChild(iFrame);
}
function close() {
drawEventClose();
}
function drawReceive(event) {
if (!event.data || event.data.length < 1) return;
let message = JSON.parse(event.data);
if (message.event === 'init') {
drawEventInit();
} else if (message.event === 'exit') {
drawEventClose();
} else if (message.event === 'save') {
drawEventSave(message);
} else if (message.event === 'export') {
drawEventExport(message);
}
}
function drawEventExport(message) {
if (onSave) {
onSave(message.data);
}
}
function drawEventSave(message) {
drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
}
function drawEventInit() {
if (!onInit) return;
onInit().then(xml => {
drawPostMessage({action: 'load', autosave: 1, xml: xml});
});
}
function drawEventClose() {
window.removeEventListener('message', drawReceive);
if (iFrame) document.body.removeChild(iFrame);
}
function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
}
module.exports = {show, close};

View File

@ -1,5 +1,6 @@
"use strict";
const Code = require('../code');
const Code = require('../libs/code');
const DrawIO = require('../libs/drawio');
/**
* Handle pasting images from clipboard.
@ -47,7 +48,7 @@ function uploadImageFile(file) {
let formData = new FormData();
formData.append('file', file, remoteFilename);
return window.$http.post('/images/gallery/upload', formData).then(resp => (resp.data));
return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
}
function registerEditorShortcuts(editor) {
@ -218,7 +219,103 @@ function codePlugin() {
});
}
codePlugin();
function drawIoPlugin() {
const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json';
let iframe = null;
let pageEditor = null;
let currentNode = null;
function isDrawing(node) {
return node.hasAttribute('drawio-diagram');
}
function showDrawingEditor(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
DrawIO.show(drawingInit, updateContent);
}
function updateContent(pngData) {
let id = "image-" + Math.random().toString(16).slice(2);
let loadingImage = window.baseUrl('/loading.gif');
let data = {
image: pngData,
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
// Handle updating an existing image
if (currentNode) {
DrawIO.close();
let imgElem = currentNode.querySelector('img');
let drawingId = currentNode.getAttribute('drawio-diagram');
window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => {
pageEditor.dom.setAttrib(imgElem, 'src', `${resp.data.url}?updated=${Date.now()}`);
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
});
return;
}
setTimeout(() => {
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
DrawIO.close();
window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => {
pageEditor.dom.setAttrib(id, 'src', resp.data.url);
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id);
}).catch(err => {
pageEditor.dom.remove(id);
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
});
}, 5);
}
function drawingInit() {
if (!currentNode) {
return Promise.resolve('');
}
let drawingId = currentNode.getAttribute('drawio-diagram');
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
return `data:image/png;base64,${resp.data.content}`;
});
}
window.tinymce.PluginManager.add('drawio', function(editor, url) {
editor.addCommand('drawio', () => {
showDrawingEditor(editor);
});
editor.addButton('drawio', {
tooltip: 'Drawing',
image: window.baseUrl('/system_images/drawing.svg'),
cmd: 'drawio'
});
editor.on('dblclick', event => {
let selectedNode = editor.selection.getNode();
if (!isDrawing(selectedNode)) return;
showDrawingEditor(editor, selectedNode);
});
editor.on('SetContent', function () {
let drawings = editor.$('body > div[drawio-diagram]');
if (!drawings.length) return;
editor.undoManager.transact(function () {
drawings.each((index, elem) => {
elem.setAttribute('contenteditable', 'false');
});
});
});
});
}
window.tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () {
@ -242,7 +339,13 @@ window.tinymce.PluginManager.add('customhr', function (editor) {
});
});
// Load plugins
let plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor";
codePlugin();
if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') {
drawIoPlugin();
plugins += ' drawio';
}
module.exports = {
selector: '#html-editor',
@ -259,12 +362,12 @@ module.exports = {
statusbar: false,
menubar: false,
paste_data_images: false,
extended_valid_elements: 'pre[*]',
extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
automatic_uploads: false,
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor",
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
plugins: plugins,
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},

View File

@ -1,5 +1,5 @@
const Clipboard = require("clipboard");
const Code = require('../code');
const Code = require('../libs/code');
let setupPageShow = window.setupPageShow = function (pageId) {

View File

@ -1,4 +1,4 @@
const codeLib = require('../code');
const codeLib = require('../libs/code');
const methods = {
show() {

View File

@ -102,6 +102,10 @@
display: flex;
align-self: flex-start;
}
.popup-content {
overflow-y: auto;
}
}
.corner-button {
@ -542,6 +546,15 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
}
@include smaller-than($m) {
#code-editor .lang-options {
max-width: 100%;
}
#code-editor .CodeMirror {
height: 200px;
}
}
.comment-box {
border: 1px solid #DDD;
margin-bottom: $-s;
@ -549,7 +562,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.content {
padding: $-s;
font-size: 0.666em;
p, ul {
p, ul, ol {
font-size: $fs-m;
margin: .5em 0;
}

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