Merge branch 'master' into release
This commit is contained in:
commit
04f7a7d301
|
@ -10,4 +10,6 @@ Homestead.yaml
|
||||||
/public/bower
|
/public/bower
|
||||||
/storage/images
|
/storage/images
|
||||||
_ide_helper.php
|
_ide_helper.php
|
||||||
/storage/debugbar
|
/storage/debugbar
|
||||||
|
.phpstorm.meta.php
|
||||||
|
yarn.lock
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php namespace BookStack;
|
||||||
|
|
||||||
|
|
||||||
|
class Attachment extends Ownable
|
||||||
|
{
|
||||||
|
protected $fillable = ['name', 'order'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the downloadable file name for this upload.
|
||||||
|
* @return mixed|string
|
||||||
|
*/
|
||||||
|
public function getFileName()
|
||||||
|
{
|
||||||
|
if (str_contains($this->name, '.')) return $this->name;
|
||||||
|
return $this->name . '.' . $this->extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the page this file was uploaded to.
|
||||||
|
* @return Page
|
||||||
|
*/
|
||||||
|
public function page()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Page::class, 'uploaded_to');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the url of this file.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUrl()
|
||||||
|
{
|
||||||
|
return baseUrl('/attachments/' . $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,9 +13,9 @@ class Book extends Entity
|
||||||
public function getUrl($path = false)
|
public function getUrl($path = false)
|
||||||
{
|
{
|
||||||
if ($path !== false) {
|
if ($path !== false) {
|
||||||
return baseUrl('/books/' . $this->slug . '/' . trim($path, '/'));
|
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||||
}
|
}
|
||||||
return baseUrl('/books/' . $this->slug);
|
return baseUrl('/books/' . urlencode($this->slug));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -32,9 +32,9 @@ class Chapter extends Entity
|
||||||
{
|
{
|
||||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||||
if ($path !== false) {
|
if ($path !== false) {
|
||||||
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/'));
|
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||||
}
|
}
|
||||||
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug);
|
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?php namespace BookStack;
|
|
||||||
|
|
||||||
class EmailConfirmation extends Model
|
|
||||||
{
|
|
||||||
protected $fillable = ['user_id', 'token'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user that this confirmation is attached to.
|
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
||||||
*/
|
|
||||||
public function user()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -162,18 +162,21 @@ class Entity extends Ownable
|
||||||
$exactTerms = [];
|
$exactTerms = [];
|
||||||
$fuzzyTerms = [];
|
$fuzzyTerms = [];
|
||||||
$search = static::newQuery();
|
$search = static::newQuery();
|
||||||
|
|
||||||
foreach ($terms as $key => $term) {
|
foreach ($terms as $key => $term) {
|
||||||
$safeTerm = htmlentities($term, ENT_QUOTES);
|
$term = htmlentities($term, ENT_QUOTES);
|
||||||
$safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm);
|
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||||
if (preg_match('/".*?"/', $safeTerm) || is_numeric($safeTerm)) {
|
if (preg_match('/".*?"/', $term) || is_numeric($term)) {
|
||||||
$safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
|
$term = str_replace('"', '', $term);
|
||||||
$exactTerms[] = '%' . $safeTerm . '%';
|
$exactTerms[] = '%' . $term . '%';
|
||||||
} else {
|
} else {
|
||||||
$safeTerm = '' . $safeTerm . '*';
|
$term = '' . $term . '*';
|
||||||
if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
|
if ($term !== '*') $fuzzyTerms[] = $term;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
|
|
||||||
|
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
|
||||||
|
|
||||||
|
|
||||||
// Perform fulltext search if relevant terms exist.
|
// Perform fulltext search if relevant terms exist.
|
||||||
if ($isFuzzy) {
|
if ($isFuzzy) {
|
||||||
|
@ -193,6 +196,7 @@ class Entity extends Ownable
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
||||||
|
|
||||||
// Add additional where terms
|
// Add additional where terms
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Events;
|
|
||||||
|
|
||||||
abstract class Event
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadException extends PrettyException {}
|
|
@ -87,4 +87,20 @@ class Handler extends ExceptionHandler
|
||||||
} while ($e = $e->getPrevious());
|
} while ($e = $e->getPrevious());
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an authentication exception into an unauthenticated response.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Illuminate\Auth\AuthenticationException $exception
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
protected function unauthenticated($request, AuthenticationException $exception)
|
||||||
|
{
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return response()->json(['error' => 'Unauthenticated.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->guest('login');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\FileUploadException;
|
||||||
|
use BookStack\Attachment;
|
||||||
|
use BookStack\Repos\PageRepo;
|
||||||
|
use BookStack\Services\AttachmentService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class AttachmentController extends Controller
|
||||||
|
{
|
||||||
|
protected $attachmentService;
|
||||||
|
protected $attachment;
|
||||||
|
protected $pageRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AttachmentController constructor.
|
||||||
|
* @param AttachmentService $attachmentService
|
||||||
|
* @param Attachment $attachment
|
||||||
|
* @param PageRepo $pageRepo
|
||||||
|
*/
|
||||||
|
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
|
||||||
|
{
|
||||||
|
$this->attachmentService = $attachmentService;
|
||||||
|
$this->attachment = $attachment;
|
||||||
|
$this->pageRepo = $pageRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint at which attachments are uploaded to.
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function upload(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
|
'file' => 'required|file'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pageId = $request->get('uploaded_to');
|
||||||
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
|
|
||||||
|
$this->checkPermission('attachment-create-all');
|
||||||
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
|
$uploadedFile = $request->file('file');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
|
||||||
|
} catch (FileUploadException $e) {
|
||||||
|
return response($e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an uploaded attachment.
|
||||||
|
* @param int $attachmentId
|
||||||
|
* @param Request $request
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function uploadUpdate($attachmentId, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
|
'file' => 'required|file'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pageId = $request->get('uploaded_to');
|
||||||
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
|
|
||||||
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||||
|
|
||||||
|
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
||||||
|
return $this->jsonError('Page mismatch during attached file update');
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadedFile = $request->file('file');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
|
||||||
|
} catch (FileUploadException $e) {
|
||||||
|
return response($e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the details of an existing file.
|
||||||
|
* @param $attachmentId
|
||||||
|
* @param Request $request
|
||||||
|
* @return Attachment|mixed
|
||||||
|
*/
|
||||||
|
public function update($attachmentId, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
|
'name' => 'required|string|min:1|max:255',
|
||||||
|
'link' => 'url|min:1|max:255'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pageId = $request->get('uploaded_to');
|
||||||
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
|
|
||||||
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||||
|
|
||||||
|
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
||||||
|
return $this->jsonError('Page mismatch during attachment update');
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a link to a page.
|
||||||
|
* @param Request $request
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function attachLink(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
|
'name' => 'required|string|min:1|max:255',
|
||||||
|
'link' => 'required|url|min:1|max:255'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pageId = $request->get('uploaded_to');
|
||||||
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
|
|
||||||
|
$this->checkPermission('attachment-create-all');
|
||||||
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
|
$attachmentName = $request->get('name');
|
||||||
|
$link = $request->get('link');
|
||||||
|
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
|
||||||
|
|
||||||
|
return response()->json($attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for a specific page.
|
||||||
|
* @param $pageId
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function listForPage($pageId)
|
||||||
|
{
|
||||||
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
return response()->json($page->attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the attachment sorting.
|
||||||
|
* @param $pageId
|
||||||
|
* @param Request $request
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function sortForPage($pageId, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'files' => 'required|array',
|
||||||
|
'files.*.id' => 'required|integer',
|
||||||
|
]);
|
||||||
|
$page = $this->pageRepo->getById($pageId);
|
||||||
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
|
$attachments = $request->get('files');
|
||||||
|
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
|
||||||
|
return response()->json(['message' => 'Attachment order updated']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an attachment from storage.
|
||||||
|
* @param $attachmentId
|
||||||
|
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function get($attachmentId)
|
||||||
|
{
|
||||||
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
|
$page = $this->pageRepo->getById($attachment->uploaded_to);
|
||||||
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
|
||||||
|
if ($attachment->external) {
|
||||||
|
return redirect($attachment->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||||
|
return response($attachmentContents, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific attachment in the system.
|
||||||
|
* @param $attachmentId
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function delete($attachmentId)
|
||||||
|
{
|
||||||
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
|
$this->checkOwnablePermission('attachment-delete', $attachment);
|
||||||
|
$this->attachmentService->deleteFile($attachment);
|
||||||
|
return response()->json(['message' => 'Attachment deleted']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Password;
|
||||||
|
|
||||||
|
class ForgotPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Reset Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller is responsible for handling password reset emails and
|
||||||
|
| includes a trait which assists in sending these notifications from
|
||||||
|
| your application to your users. Feel free to explore this trait.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use SendsPasswordResetEmails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('guest');
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reset link to the given user.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function sendResetLinkEmail(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, ['email' => 'required|email']);
|
||||||
|
|
||||||
|
// We will send the password reset link to this user. Once we have attempted
|
||||||
|
// to send the link, we will examine the response then see the message we
|
||||||
|
// need to show to the user. Finally, we'll send out a proper response.
|
||||||
|
$response = $this->broker()->sendResetLink(
|
||||||
|
$request->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($response === Password::RESET_LINK_SENT) {
|
||||||
|
$message = 'A password reset link has been sent to ' . $request->get('email') . '.';
|
||||||
|
session()->flash('success', $message);
|
||||||
|
return back()->with('status', trans($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an error was returned by the password broker, we will get this message
|
||||||
|
// translated so we can notify a user of the problem. We'll redirect back
|
||||||
|
// to where the users came from so they can attempt this process again.
|
||||||
|
return back()->withErrors(
|
||||||
|
['email' => trans($response)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use BookStack\Repos\UserRepo;
|
||||||
|
use BookStack\Services\SocialAuthService;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LoginController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Login Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller handles authenticating users for the application and
|
||||||
|
| redirecting them to your home screen. The controller uses a trait
|
||||||
|
| to conveniently provide its functionality to your applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use AuthenticatesUsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where to redirect users after login.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $redirectTo = '/';
|
||||||
|
|
||||||
|
protected $redirectPath = '/';
|
||||||
|
protected $redirectAfterLogout = '/login';
|
||||||
|
|
||||||
|
protected $socialAuthService;
|
||||||
|
protected $userRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @param SocialAuthService $socialAuthService
|
||||||
|
* @param UserRepo $userRepo
|
||||||
|
*/
|
||||||
|
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
|
||||||
|
{
|
||||||
|
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||||
|
$this->socialAuthService = $socialAuthService;
|
||||||
|
$this->userRepo = $userRepo;
|
||||||
|
$this->redirectPath = baseUrl('/');
|
||||||
|
$this->redirectAfterLogout = baseUrl('/login');
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function username()
|
||||||
|
{
|
||||||
|
return config('auth.method') === 'standard' ? 'email' : 'username';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the action when a user is authenticated.
|
||||||
|
* If the user authenticated but does not exist in the user table we create them.
|
||||||
|
* @param Request $request
|
||||||
|
* @param Authenticatable $user
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
* @throws AuthException
|
||||||
|
*/
|
||||||
|
protected function authenticated(Request $request, Authenticatable $user)
|
||||||
|
{
|
||||||
|
// Explicitly log them out for now if they do no exist.
|
||||||
|
if (!$user->exists) auth()->logout($user);
|
||||||
|
|
||||||
|
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||||
|
$request->flash();
|
||||||
|
session()->flash('request-email', true);
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||||
|
$user->email = $request->get('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->exists) {
|
||||||
|
|
||||||
|
// Check for users with same email already
|
||||||
|
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||||
|
if ($alreadyUser) {
|
||||||
|
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
$this->userRepo->attachDefaultRole($user);
|
||||||
|
auth()->login($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = session()->pull('url.intended', '/');
|
||||||
|
$path = baseUrl($path, true);
|
||||||
|
return redirect($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the application login form.
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function getLogin()
|
||||||
|
{
|
||||||
|
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||||
|
$authMethod = config('auth.method');
|
||||||
|
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the relevant social site.
|
||||||
|
* @param $socialDriver
|
||||||
|
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function getSocialLogin($socialDriver)
|
||||||
|
{
|
||||||
|
session()->put('social-callback', 'login');
|
||||||
|
return $this->socialAuthService->startLogIn($socialDriver);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,76 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Password;
|
|
||||||
|
|
||||||
class PasswordController extends Controller
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Password Reset Controller
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This controller is responsible for handling password reset requests
|
|
||||||
| and uses a simple trait to include this behavior. You're free to
|
|
||||||
| explore this trait and override any methods you wish to tweak.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
use ResetsPasswords;
|
|
||||||
|
|
||||||
protected $redirectTo = '/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new password controller instance.
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware('guest');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a reset link to the given user.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function sendResetLinkEmail(Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, ['email' => 'required|email']);
|
|
||||||
|
|
||||||
$broker = $this->getBroker();
|
|
||||||
|
|
||||||
$response = Password::broker($broker)->sendResetLink(
|
|
||||||
$request->only('email'), $this->resetEmailBuilder()
|
|
||||||
);
|
|
||||||
|
|
||||||
switch ($response) {
|
|
||||||
case Password::RESET_LINK_SENT:
|
|
||||||
$message = 'A password reset link has been sent to ' . $request->get('email') . '.';
|
|
||||||
session()->flash('success', $message);
|
|
||||||
return $this->getSendResetLinkEmailSuccessResponse($response);
|
|
||||||
|
|
||||||
case Password::INVALID_USER:
|
|
||||||
default:
|
|
||||||
return $this->getSendResetLinkEmailFailureResponse($response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the response for after a successful password reset.
|
|
||||||
*
|
|
||||||
* @param string $response
|
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
|
||||||
*/
|
|
||||||
protected function getResetSuccessResponse($response)
|
|
||||||
{
|
|
||||||
$message = 'Your password has been successfully reset.';
|
|
||||||
session()->flash('success', $message);
|
|
||||||
return redirect($this->redirectPath())->with('status', trans($response));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +1,68 @@
|
||||||
<?php namespace BookStack\Http\Controllers\Auth;
|
<?php
|
||||||
|
|
||||||
use BookStack\Exceptions\AuthException;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
|
||||||
use Illuminate\Http\Request;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Exceptions\SocialSignInException;
|
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Repos\UserRepo;
|
use BookStack\Repos\UserRepo;
|
||||||
use BookStack\Services\EmailConfirmationService;
|
use BookStack\Services\EmailConfirmationService;
|
||||||
use BookStack\Services\SocialAuthService;
|
use BookStack\Services\SocialAuthService;
|
||||||
use BookStack\SocialAccount;
|
use BookStack\User;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
use Validator;
|
use Validator;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
|
|
||||||
|
|
||||||
class AuthController extends Controller
|
class RegisterController extends Controller
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Registration & Login Controller
|
| Register Controller
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This controller handles the registration of new users, as well as the
|
| This controller handles the registration of new users as well as their
|
||||||
| authentication of existing users. By default, this controller uses
|
| validation and creation. By default this controller uses a trait to
|
||||||
| a simple trait to add these behaviors. Why don't you explore it?
|
| provide this functionality without requiring any additional code.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
|
use RegistersUsers;
|
||||||
|
|
||||||
protected $redirectPath = '/';
|
|
||||||
protected $redirectAfterLogout = '/login';
|
|
||||||
protected $username = 'email';
|
|
||||||
|
|
||||||
protected $socialAuthService;
|
protected $socialAuthService;
|
||||||
protected $emailConfirmationService;
|
protected $emailConfirmationService;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new authentication controller instance.
|
* Where to redirect users after login / registration.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $redirectTo = '/';
|
||||||
|
protected $redirectPath = '/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
* @param SocialAuthService $socialAuthService
|
* @param SocialAuthService $socialAuthService
|
||||||
* @param EmailConfirmationService $emailConfirmationService
|
* @param EmailConfirmationService $emailConfirmationService
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]);
|
$this->middleware('guest');
|
||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialAuthService = $socialAuthService;
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
$this->emailConfirmationService = $emailConfirmationService;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
|
$this->redirectTo = baseUrl('/');
|
||||||
$this->redirectPath = baseUrl('/');
|
$this->redirectPath = baseUrl('/');
|
||||||
$this->redirectAfterLogout = baseUrl('/login');
|
|
||||||
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a validator for an incoming registration request.
|
* Get a validator for an incoming registration request.
|
||||||
|
*
|
||||||
* @param array $data
|
* @param array $data
|
||||||
* @return \Illuminate\Contracts\Validation\Validator
|
* @return \Illuminate\Contracts\Validation\Validator
|
||||||
*/
|
*/
|
||||||
|
@ -69,6 +75,10 @@ class AuthController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not registrations are allowed in the app settings.
|
||||||
|
* @throws UserRegistrationException
|
||||||
|
*/
|
||||||
protected function checkRegistrationAllowed()
|
protected function checkRegistrationAllowed()
|
||||||
{
|
{
|
||||||
if (!setting('registration-enabled')) {
|
if (!setting('registration-enabled')) {
|
||||||
|
@ -78,7 +88,7 @@ class AuthController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the application registration form.
|
* Show the application registration form.
|
||||||
* @return \Illuminate\Http\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function getRegister()
|
public function getRegister()
|
||||||
{
|
{
|
||||||
|
@ -89,9 +99,10 @@ class AuthController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a registration request for the application.
|
* Handle a registration request for the application.
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param Request|\Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Http\Response
|
* @return Response
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
|
* @throws \Illuminate\Foundation\Validation\ValidationException
|
||||||
*/
|
*/
|
||||||
public function postRegister(Request $request)
|
public function postRegister(Request $request)
|
||||||
{
|
{
|
||||||
|
@ -108,66 +119,18 @@ class AuthController extends Controller
|
||||||
return $this->registerUser($userData);
|
return $this->registerUser($userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the action when a user is authenticated.
|
* Create a new user instance after a valid registration.
|
||||||
* If the user authenticated but does not exist in the user table we create them.
|
* @param array $data
|
||||||
* @param Request $request
|
* @return User
|
||||||
* @param Authenticatable $user
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
* @throws AuthException
|
|
||||||
*/
|
*/
|
||||||
protected function authenticated(Request $request, Authenticatable $user)
|
protected function create(array $data)
|
||||||
{
|
{
|
||||||
// Explicitly log them out for now if they do no exist.
|
return User::create([
|
||||||
if (!$user->exists) auth()->logout($user);
|
'name' => $data['name'],
|
||||||
|
'email' => $data['email'],
|
||||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
'password' => bcrypt($data['password']),
|
||||||
$request->flash();
|
]);
|
||||||
session()->flash('request-email', true);
|
|
||||||
return redirect('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
|
||||||
$user->email = $request->get('email');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user->exists) {
|
|
||||||
|
|
||||||
// Check for users with same email already
|
|
||||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
|
||||||
if ($alreadyUser) {
|
|
||||||
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->save();
|
|
||||||
$this->userRepo->attachDefaultRole($user);
|
|
||||||
auth()->login($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
$path = session()->pull('url.intended', '/');
|
|
||||||
$path = baseUrl($path, true);
|
|
||||||
return redirect($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new user after a registration callback.
|
|
||||||
* @param $socialDriver
|
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
|
||||||
* @throws UserRegistrationException
|
|
||||||
*/
|
|
||||||
protected function socialRegisterCallback($socialDriver)
|
|
||||||
{
|
|
||||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
|
||||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
|
||||||
|
|
||||||
// Create an array of the user data to create a new user instance
|
|
||||||
$userData = [
|
|
||||||
'name' => $socialUser->getName(),
|
|
||||||
'email' => $socialUser->getEmail(),
|
|
||||||
'password' => str_random(30)
|
|
||||||
];
|
|
||||||
return $this->registerUser($userData, $socialAccount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -176,7 +139,7 @@ class AuthController extends Controller
|
||||||
* @param bool|false|SocialAccount $socialAccount
|
* @param bool|false|SocialAccount $socialAccount
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
* @throws \BookStack\Exceptions\ConfirmationEmailException
|
* @throws ConfirmationEmailException
|
||||||
*/
|
*/
|
||||||
protected function registerUser(array $userData, $socialAccount = false)
|
protected function registerUser(array $userData, $socialAccount = false)
|
||||||
{
|
{
|
||||||
|
@ -195,7 +158,13 @@ class AuthController extends Controller
|
||||||
|
|
||||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||||
$newUser->save();
|
$newUser->save();
|
||||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
|
||||||
|
try {
|
||||||
|
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect('/register/confirm');
|
return redirect('/register/confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,18 +182,6 @@ class AuthController extends Controller
|
||||||
return view('auth/register-confirm');
|
return view('auth/register-confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* View the confirmation email as a standard web page.
|
|
||||||
* @param $token
|
|
||||||
* @return \Illuminate\View\View
|
|
||||||
* @throws UserRegistrationException
|
|
||||||
*/
|
|
||||||
public function viewConfirmEmail($token)
|
|
||||||
{
|
|
||||||
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
|
|
||||||
return view('emails/email-confirmation', ['token' => $confirmation->token]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirms an email via a token and logs the user into the system.
|
* Confirms an email via a token and logs the user into the system.
|
||||||
* @param $token
|
* @param $token
|
||||||
|
@ -237,8 +194,8 @@ class AuthController extends Controller
|
||||||
$user = $confirmation->user;
|
$user = $confirmation->user;
|
||||||
$user->email_confirmed = true;
|
$user->email_confirmed = true;
|
||||||
$user->save();
|
$user->save();
|
||||||
auth()->login($confirmation->user);
|
auth()->login($user);
|
||||||
session()->flash('success', 'Your email has been confirmed!');
|
session()->flash('success', trans('auth.email_confirm_success'));
|
||||||
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
||||||
return redirect($this->redirectPath);
|
return redirect($this->redirectPath);
|
||||||
}
|
}
|
||||||
|
@ -264,33 +221,19 @@ class AuthController extends Controller
|
||||||
'email' => 'required|email|exists:users,email'
|
'email' => 'required|email|exists:users,email'
|
||||||
]);
|
]);
|
||||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->emailConfirmationService->sendConfirmation($user);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||||
|
return redirect('/register/confirm');
|
||||||
|
}
|
||||||
|
|
||||||
$this->emailConfirmationService->sendConfirmation($user);
|
$this->emailConfirmationService->sendConfirmation($user);
|
||||||
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
|
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||||
return redirect('/register/confirm');
|
return redirect('/register/confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the application login form.
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function getLogin()
|
|
||||||
{
|
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
|
||||||
$authMethod = config('auth.method');
|
|
||||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect to the relevant social site.
|
|
||||||
* @param $socialDriver
|
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
|
||||||
*/
|
|
||||||
public function getSocialLogin($socialDriver)
|
|
||||||
{
|
|
||||||
session()->put('social-callback', 'login');
|
|
||||||
return $this->socialAuthService->startLogIn($socialDriver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the social site for authentication intended to register.
|
* Redirect to the social site for authentication intended to register.
|
||||||
* @param $socialDriver
|
* @param $socialDriver
|
||||||
|
@ -334,4 +277,25 @@ class AuthController extends Controller
|
||||||
return $this->socialAuthService->detachSocialAccount($socialDriver);
|
return $this->socialAuthService->detachSocialAccount($socialDriver);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* Register a new user after a registration callback.
|
||||||
|
* @param $socialDriver
|
||||||
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
|
* @throws UserRegistrationException
|
||||||
|
*/
|
||||||
|
protected function socialRegisterCallback($socialDriver)
|
||||||
|
{
|
||||||
|
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||||
|
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||||
|
|
||||||
|
// Create an array of the user data to create a new user instance
|
||||||
|
$userData = [
|
||||||
|
'name' => $socialUser->getName(),
|
||||||
|
'email' => $socialUser->getEmail(),
|
||||||
|
'password' => str_random(30)
|
||||||
|
];
|
||||||
|
return $this->registerUser($userData, $socialAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||||
|
|
||||||
|
class ResetPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Reset Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller is responsible for handling password reset requests
|
||||||
|
| and uses a simple trait to include this behavior. You're free to
|
||||||
|
| explore this trait and override any methods you wish to tweak.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use ResetsPasswords;
|
||||||
|
|
||||||
|
protected $redirectTo = '/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('guest');
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the response for a successful password reset.
|
||||||
|
*
|
||||||
|
* @param string $response
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
protected function sendResetResponse($response)
|
||||||
|
{
|
||||||
|
$message = 'Your password has been successfully reset.';
|
||||||
|
session()->flash('success', $message);
|
||||||
|
return redirect($this->redirectPath())
|
||||||
|
->with('status', trans($response));
|
||||||
|
}
|
||||||
|
}
|
|
@ -115,9 +115,11 @@ class ChapterController extends Controller
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
if ($chapter->name !== $request->get('name')) {
|
||||||
|
$chapter->slug = $this->chapterRepo->findSuitableSlug($request->get('name'), $book->id, $chapter->id);
|
||||||
|
}
|
||||||
$chapter->fill($request->all());
|
$chapter->fill($request->all());
|
||||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
|
$chapter->updated_by = user()->id;
|
||||||
$chapter->updated_by = auth()->user()->id;
|
|
||||||
$chapter->save();
|
$chapter->save();
|
||||||
Activity::add($chapter, 'chapter_update', $book->id);
|
Activity::add($chapter, 'chapter_update', $book->id);
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
|
|
|
@ -3,13 +3,11 @@
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
use HttpRequestException;
|
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
use Illuminate\Http\Exception\HttpResponseException;
|
use Illuminate\Http\Exception\HttpResponseException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Session;
|
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
|
|
||||||
abstract class Controller extends BaseController
|
abstract class Controller extends BaseController
|
||||||
|
@ -30,17 +28,21 @@ abstract class Controller extends BaseController
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
// Get a user instance for the current user
|
$this->middleware(function ($request, $next) {
|
||||||
$user = auth()->user();
|
|
||||||
if (!$user) $user = User::getDefault();
|
|
||||||
|
|
||||||
// Share variables with views
|
// Get a user instance for the current user
|
||||||
view()->share('signedIn', auth()->check());
|
$user = user();
|
||||||
view()->share('currentUser', $user);
|
|
||||||
|
|
||||||
// Share variables with controllers
|
// Share variables with controllers
|
||||||
$this->currentUser = $user;
|
$this->currentUser = $user;
|
||||||
$this->signedIn = auth()->check();
|
$this->signedIn = auth()->check();
|
||||||
|
|
||||||
|
// Share variables with views
|
||||||
|
view()->share('signedIn', $this->signedIn);
|
||||||
|
view()->share('currentUser', $user);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,8 +69,13 @@ abstract class Controller extends BaseController
|
||||||
*/
|
*/
|
||||||
protected function showPermissionError()
|
protected function showPermissionError()
|
||||||
{
|
{
|
||||||
Session::flash('error', trans('errors.permission'));
|
if (request()->wantsJson()) {
|
||||||
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
|
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
|
||||||
|
} else {
|
||||||
|
$response = redirect('/');
|
||||||
|
session()->flash('error', trans('errors.permission'));
|
||||||
|
}
|
||||||
|
|
||||||
throw new HttpResponseException($response);
|
throw new HttpResponseException($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +86,7 @@ abstract class Controller extends BaseController
|
||||||
*/
|
*/
|
||||||
protected function checkPermission($permissionName)
|
protected function checkPermission($permissionName)
|
||||||
{
|
{
|
||||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
if (!user() || !user()->can($permissionName)) {
|
||||||
$this->showPermissionError();
|
$this->showPermissionError();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -121,4 +128,22 @@ abstract class Controller extends BaseController
|
||||||
return response()->json(['message' => $messageText], $statusCode);
|
return response()->json(['message' => $messageText], $statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the response for when a request fails validation.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param array $errors
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
protected function buildFailedValidationResponse(Request $request, array $errors)
|
||||||
|
{
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return response()->json(['validation' => $errors], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to($this->getRedirectUrl())
|
||||||
|
->withInput($request->input())
|
||||||
|
->withErrors($errors, $this->errorBag());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use BookStack\Repos\ChapterRepo;
|
||||||
use BookStack\Repos\PageRepo;
|
use BookStack\Repos\PageRepo;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Views;
|
use Views;
|
||||||
|
use GatherContent\Htmldiff\Htmldiff;
|
||||||
|
|
||||||
class PageController extends Controller
|
class PageController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -42,27 +43,60 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for creating a new page.
|
* Show the form for creating a new page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param bool $chapterSlug
|
* @param string $chapterSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @internal param bool $pageSlug
|
* @internal param bool $pageSlug
|
||||||
*/
|
*/
|
||||||
public function create($bookSlug, $chapterSlug = false)
|
public function create($bookSlug, $chapterSlug = null)
|
||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
||||||
$parent = $chapter ? $chapter : $book;
|
$parent = $chapter ? $chapter : $book;
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
$this->setPageTitle('Create New Page');
|
|
||||||
|
|
||||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
// Redirect to draft edit screen if signed in
|
||||||
return redirect($draft->getUrl());
|
if ($this->signedIn) {
|
||||||
|
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
||||||
|
return redirect($draft->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise show edit view
|
||||||
|
$this->setPageTitle('Create New Page');
|
||||||
|
return view('pages/guest-create', ['parent' => $parent]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new page as a guest user.
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $bookSlug
|
||||||
|
* @param string|null $chapterSlug
|
||||||
|
* @return mixed
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'name' => 'required|string|max:255'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
|
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
||||||
|
$parent = $chapter ? $chapter : $book;
|
||||||
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
|
$page = $this->pageRepo->getDraftPage($book, $chapter);
|
||||||
|
$this->pageRepo->publishDraft($page, [
|
||||||
|
'name' => $request->get('name'),
|
||||||
|
'html' => ''
|
||||||
|
]);
|
||||||
|
return redirect($page->getUrl('/edit'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show form to continue editing a draft page.
|
* Show form to continue editing a draft page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageId
|
* @param int $pageId
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function editDraft($bookSlug, $pageId)
|
public function editDraft($bookSlug, $pageId)
|
||||||
|
@ -72,7 +106,13 @@ class PageController extends Controller
|
||||||
$this->checkOwnablePermission('page-create', $book);
|
$this->checkOwnablePermission('page-create', $book);
|
||||||
$this->setPageTitle('Edit Page Draft');
|
$this->setPageTitle('Edit Page Draft');
|
||||||
|
|
||||||
return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
|
$draftsEnabled = $this->signedIn;
|
||||||
|
return view('pages/edit', [
|
||||||
|
'page' => $draft,
|
||||||
|
'book' => $book,
|
||||||
|
'isDraft' => true,
|
||||||
|
'draftsEnabled' => $draftsEnabled
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,8 +152,8 @@ class PageController extends Controller
|
||||||
* Display the specified page.
|
* Display the specified page.
|
||||||
* If the page is not found via the slug the
|
* If the page is not found via the slug the
|
||||||
* revisions are searched for a match.
|
* revisions are searched for a match.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function show($bookSlug, $pageSlug)
|
public function show($bookSlug, $pageSlug)
|
||||||
|
@ -131,14 +171,17 @@ class PageController extends Controller
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
|
||||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||||
|
$pageNav = $this->pageRepo->getPageNav($page);
|
||||||
|
|
||||||
Views::add($page);
|
Views::add($page);
|
||||||
$this->setPageTitle($page->getShortName());
|
$this->setPageTitle($page->getShortName());
|
||||||
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
|
return view('pages/show', ['page' => $page, 'book' => $book,
|
||||||
|
'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page from an ajax request.
|
* Get page from an ajax request.
|
||||||
* @param $pageId
|
* @param int $pageId
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
public function getPageAjax($pageId)
|
public function getPageAjax($pageId)
|
||||||
|
@ -149,8 +192,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for editing the specified page.
|
* Show the form for editing the specified page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function edit($bookSlug, $pageSlug)
|
public function edit($bookSlug, $pageSlug)
|
||||||
|
@ -179,14 +222,20 @@ class PageController extends Controller
|
||||||
|
|
||||||
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||||
|
|
||||||
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
|
$draftsEnabled = $this->signedIn;
|
||||||
|
return view('pages/edit', [
|
||||||
|
'page' => $page,
|
||||||
|
'book' => $book,
|
||||||
|
'current' => $page,
|
||||||
|
'draftsEnabled' => $draftsEnabled
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified page in storage.
|
* Update the specified page in storage.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $bookSlug, $pageSlug)
|
public function update(Request $request, $bookSlug, $pageSlug)
|
||||||
|
@ -205,13 +254,21 @@ class PageController extends Controller
|
||||||
/**
|
/**
|
||||||
* Save a draft update as a revision.
|
* Save a draft update as a revision.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param $pageId
|
* @param int $pageId
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
public function saveDraft(Request $request, $pageId)
|
public function saveDraft(Request $request, $pageId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById($pageId, true);
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
|
if (!$this->signedIn) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Guests cannot save drafts',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
if ($page->draft) {
|
if ($page->draft) {
|
||||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||||
} else {
|
} else {
|
||||||
|
@ -230,7 +287,7 @@ class PageController extends Controller
|
||||||
/**
|
/**
|
||||||
* Redirect from a special link url which
|
* Redirect from a special link url which
|
||||||
* uses the page id rather than the name.
|
* uses the page id rather than the name.
|
||||||
* @param $pageId
|
* @param int $pageId
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
*/
|
*/
|
||||||
public function redirectFromLink($pageId)
|
public function redirectFromLink($pageId)
|
||||||
|
@ -241,8 +298,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the deletion page for the specified page.
|
* Show the deletion page for the specified page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showDelete($bookSlug, $pageSlug)
|
public function showDelete($bookSlug, $pageSlug)
|
||||||
|
@ -257,8 +314,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the deletion page for the specified page.
|
* Show the deletion page for the specified page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageId
|
* @param int $pageId
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
|
@ -273,8 +330,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the specified page from storage.
|
* Remove the specified page from storage.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @internal param int $id
|
* @internal param int $id
|
||||||
*/
|
*/
|
||||||
|
@ -291,8 +348,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the specified draft page from storage.
|
* Remove the specified draft page from storage.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageId
|
* @param int $pageId
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
|
@ -308,8 +365,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the last revisions for this page.
|
* Shows the last revisions for this page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showRevisions($bookSlug, $pageSlug)
|
public function showRevisions($bookSlug, $pageSlug)
|
||||||
|
@ -322,9 +379,9 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a preview of a single revision
|
* Shows a preview of a single revision
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @param $revisionId
|
* @param int $revisionId
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
||||||
|
@ -332,16 +389,48 @@ class PageController extends Controller
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||||
$revision = $this->pageRepo->getRevisionById($revisionId);
|
$revision = $this->pageRepo->getRevisionById($revisionId);
|
||||||
|
|
||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
$this->setPageTitle('Page Revision For ' . $page->getShortName());
|
$this->setPageTitle('Page Revision For ' . $page->getShortName());
|
||||||
return view('pages/revision', ['page' => $page, 'book' => $book]);
|
|
||||||
|
return view('pages/revision', [
|
||||||
|
'page' => $page,
|
||||||
|
'book' => $book,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the changes of a single revision
|
||||||
|
* @param string $bookSlug
|
||||||
|
* @param string $pageSlug
|
||||||
|
* @param int $revisionId
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
|
||||||
|
{
|
||||||
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
|
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||||
|
$revision = $this->pageRepo->getRevisionById($revisionId);
|
||||||
|
|
||||||
|
$prev = $revision->getPrevious();
|
||||||
|
$prevContent = ($prev === null) ? '' : $prev->html;
|
||||||
|
$diff = (new Htmldiff)->diff($prevContent, $revision->html);
|
||||||
|
|
||||||
|
$page->fill($revision->toArray());
|
||||||
|
$this->setPageTitle('Page Revision For ' . $page->getShortName());
|
||||||
|
|
||||||
|
return view('pages/revision', [
|
||||||
|
'page' => $page,
|
||||||
|
'book' => $book,
|
||||||
|
'diff' => $diff,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a page using the content of the specified revision.
|
* Restores a page using the content of the specified revision.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @param $revisionId
|
* @param int $revisionId
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
*/
|
*/
|
||||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||||
|
@ -357,8 +446,8 @@ class PageController extends Controller
|
||||||
/**
|
/**
|
||||||
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
|
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
|
||||||
* https://github.com/barryvdh/laravel-dompdf
|
* https://github.com/barryvdh/laravel-dompdf
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function exportPdf($bookSlug, $pageSlug)
|
public function exportPdf($bookSlug, $pageSlug)
|
||||||
|
@ -374,8 +463,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a page to a self-contained HTML file.
|
* Export a page to a self-contained HTML file.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function exportHtml($bookSlug, $pageSlug)
|
public function exportHtml($bookSlug, $pageSlug)
|
||||||
|
@ -391,8 +480,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a page to a simple plaintext .txt file.
|
* Export a page to a simple plaintext .txt file.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function exportPlainText($bookSlug, $pageSlug)
|
public function exportPlainText($bookSlug, $pageSlug)
|
||||||
|
@ -434,8 +523,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the Restrictions view.
|
* Show the Restrictions view.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showRestrict($bookSlug, $pageSlug)
|
public function showRestrict($bookSlug, $pageSlug)
|
||||||
|
@ -452,8 +541,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the view to choose a new parent to move a page into.
|
* Show the view to choose a new parent to move a page into.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
|
@ -470,8 +559,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the action of moving the location of a page
|
* Does the action of moving the location of a page
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
|
@ -513,8 +602,8 @@ class PageController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the permissions for this page.
|
* Set the permissions for this page.
|
||||||
* @param $bookSlug
|
* @param string $bookSlug
|
||||||
* @param $pageSlug
|
* @param string $pageSlug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,10 +17,7 @@ class SettingController extends Controller
|
||||||
$this->setPageTitle('Settings');
|
$this->setPageTitle('Settings');
|
||||||
|
|
||||||
// Get application version
|
// Get application version
|
||||||
$version = false;
|
$version = trim(file_get_contents(base_path('version')));
|
||||||
if (function_exists('exec')) {
|
|
||||||
$version = exec('git describe --always --tags ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('settings/index', ['version' => $version]);
|
return view('settings/index', ['version' => $version]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Activity;
|
use BookStack\Activity;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
@ -56,7 +57,7 @@ class UserController extends Controller
|
||||||
{
|
{
|
||||||
$this->checkPermission('users-manage');
|
$this->checkPermission('users-manage');
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$roles = $this->userRepo->getAssignableRoles();
|
$roles = $this->userRepo->getAllRoles();
|
||||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +101,14 @@ class UserController extends Controller
|
||||||
|
|
||||||
// Get avatar from gravatar and save
|
// Get avatar from gravatar and save
|
||||||
if (!config('services.disable_services')) {
|
if (!config('services.disable_services')) {
|
||||||
$avatar = \Images::saveUserGravatar($user);
|
try {
|
||||||
$user->avatar()->associate($avatar);
|
$avatar = \Images::saveUserGravatar($user);
|
||||||
$user->save();
|
$user->avatar()->associate($avatar);
|
||||||
|
$user->save();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
\Log::error('Failed to save user gravatar image');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect('/settings/users');
|
return redirect('/settings/users');
|
||||||
|
@ -120,12 +126,13 @@ class UserController extends Controller
|
||||||
return $this->currentUser->id == $id;
|
return $this->currentUser->id == $id;
|
||||||
});
|
});
|
||||||
|
|
||||||
$authMethod = config('auth.method');
|
|
||||||
|
|
||||||
$user = $this->user->findOrFail($id);
|
$user = $this->user->findOrFail($id);
|
||||||
|
|
||||||
|
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
||||||
|
|
||||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||||
$this->setPageTitle('User Profile');
|
$this->setPageTitle('User Profile');
|
||||||
$roles = $this->userRepo->getAssignableRoles();
|
$roles = $this->userRepo->getAllRoles();
|
||||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +187,7 @@ class UserController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the user delete page.
|
* Show the user delete page.
|
||||||
* @param $id
|
* @param int $id
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function delete($id)
|
public function delete($id)
|
||||||
|
@ -213,6 +220,11 @@ class UserController extends Controller
|
||||||
return redirect($user->getEditUrl());
|
return redirect($user->getEditUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($user->system_name === 'public') {
|
||||||
|
session()->flash('error', 'You cannot delete the guest user');
|
||||||
|
return redirect($user->getEditUrl());
|
||||||
|
}
|
||||||
|
|
||||||
$this->userRepo->destroy($user);
|
$this->userRepo->destroy($user);
|
||||||
session()->flash('success', 'User successfully removed');
|
session()->flash('success', 'User successfully removed');
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,32 @@ class Kernel extends HttpKernel
|
||||||
/**
|
/**
|
||||||
* The application's global HTTP middleware stack.
|
* The application's global HTTP middleware stack.
|
||||||
*
|
*
|
||||||
|
* These middleware are run during every request to your application.
|
||||||
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $middleware = [
|
protected $middleware = [
|
||||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
];
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
/**
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
* The application's route middleware groups.
|
||||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $middlewareGroups = [
|
||||||
|
'web' => [
|
||||||
|
\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,
|
||||||
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
],
|
||||||
|
'api' => [
|
||||||
|
'throttle:60,1',
|
||||||
|
'bindings',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +43,7 @@ class Kernel extends HttpKernel
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $routeMiddleware = [
|
protected $routeMiddleware = [
|
||||||
|
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||||
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||||
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Authenticate
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||||
return redirect()->guest(baseUrl('/register/confirm/awaiting'));
|
return redirect(baseUrl('/register/confirm/awaiting'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->auth->guest() && !setting('app-public')) {
|
if ($this->auth->guest() && !setting('app-public')) {
|
||||||
|
|
|
@ -34,7 +34,8 @@ class RedirectIfAuthenticated
|
||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
if ($this->auth->check()) {
|
$requireConfirmation = setting('registration-confirmation');
|
||||||
|
if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Jobs;
|
|
||||||
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
|
|
||||||
abstract class Job
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Queueable Jobs
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This job base class provides a central location to place any logic that
|
|
||||||
| is shared across all of your jobs. The trait included with the class
|
|
||||||
| provides access to the "queueOn" and "delay" queue helper methods.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
use Queueable;
|
|
||||||
}
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class ConfirmEmail extends Notification
|
||||||
|
{
|
||||||
|
|
||||||
|
public $token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
* @param string $token
|
||||||
|
*/
|
||||||
|
public function __construct($token)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function via($notifiable)
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable)
|
||||||
|
{
|
||||||
|
$appName = ['appName' => setting('app-name')];
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject(trans('auth.email_confirm_subject', $appName))
|
||||||
|
->greeting(trans('auth.email_confirm_greeting', $appName))
|
||||||
|
->line(trans('auth.email_confirm_text'))
|
||||||
|
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class ResetPassword extends Notification
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The password reset token.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a notification instance.
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
*/
|
||||||
|
public function __construct($token)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's channels.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array|string
|
||||||
|
*/
|
||||||
|
public function via($notifiable)
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the mail representation of the notification.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||||
|
*/
|
||||||
|
public function toMail()
|
||||||
|
{
|
||||||
|
return (new MailMessage)
|
||||||
|
->line('You are receiving this email because we received a password reset request for your account.')
|
||||||
|
->action('Reset Password', baseUrl('password/reset/' . $this->token))
|
||||||
|
->line('If you did not request a password reset, no further action is required.');
|
||||||
|
}
|
||||||
|
}
|
15
app/Page.php
15
app/Page.php
|
@ -54,6 +54,15 @@ class Page extends Entity
|
||||||
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments assigned to this page.
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function attachments()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this page.
|
* Get the url for this page.
|
||||||
* @param string|bool $path
|
* @param string|bool $path
|
||||||
|
@ -63,13 +72,13 @@ class Page extends Entity
|
||||||
{
|
{
|
||||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||||
$midText = $this->draft ? '/draft/' : '/page/';
|
$midText = $this->draft ? '/draft/' : '/page/';
|
||||||
$idComponent = $this->draft ? $this->id : $this->slug;
|
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
|
||||||
|
|
||||||
if ($path !== false) {
|
if ($path !== false) {
|
||||||
return baseUrl('/books/' . $bookSlug . $midText . $idComponent . '/' . trim($path, '/'));
|
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseUrl('/books/' . $bookSlug . $midText . $idComponent);
|
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,11 +25,26 @@ class PageRevision extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this revision.
|
* Get the url for this revision.
|
||||||
|
* @param null|string $path
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getUrl()
|
public function getUrl($path = null)
|
||||||
{
|
{
|
||||||
return $this->page->getUrl() . '/revisions/' . $this->id;
|
$url = $this->page->getUrl() . '/revisions/' . $this->id;
|
||||||
|
if ($path) return $url . '/' . trim($path, '/');
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the previous revision for the same page if existing
|
||||||
|
* @return \BookStack\PageRevision|null
|
||||||
|
*/
|
||||||
|
public function getPrevious()
|
||||||
|
{
|
||||||
|
if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
|
||||||
|
return static::find($id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
|
||||||
|
class BroadcastServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
// Broadcast::routes();
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// * Authenticate the user's personal channel...
|
||||||
|
// */
|
||||||
|
// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
|
||||||
|
// return (int) $user->id === (int) $userId;
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,13 +21,10 @@ class EventServiceProvider extends ServiceProvider
|
||||||
/**
|
/**
|
||||||
* Register any other events for your application.
|
* Register any other events for your application.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function boot(DispatcherContract $events)
|
public function boot()
|
||||||
{
|
{
|
||||||
parent::boot($events);
|
parent::boot();
|
||||||
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<?php namespace BookStack\Providers;
|
<?php namespace BookStack\Providers;
|
||||||
|
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
|
||||||
use Illuminate\Pagination\Paginator;
|
use Illuminate\Pagination\Paginator;
|
||||||
|
|
||||||
class PaginationServiceProvider extends ServiceProvider
|
class PaginationServiceProvider extends IlluminatePaginationServiceProvider
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the service provider.
|
* Register the service provider.
|
||||||
*
|
*
|
||||||
|
@ -13,6 +14,10 @@ class PaginationServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
|
Paginator::viewFactoryResolver(function () {
|
||||||
|
return $this->app['view'];
|
||||||
|
});
|
||||||
|
|
||||||
Paginator::currentPathResolver(function () {
|
Paginator::currentPathResolver(function () {
|
||||||
return baseUrl($this->app['request']->path());
|
return baseUrl($this->app['request']->path());
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace BookStack\Providers;
|
||||||
|
|
||||||
use Illuminate\Routing\Router;
|
use Illuminate\Routing\Router;
|
||||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
|
use Route;
|
||||||
|
|
||||||
class RouteServiceProvider extends ServiceProvider
|
class RouteServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
|
||||||
/**
|
/**
|
||||||
* Define your route model bindings, pattern filters, etc.
|
* Define your route model bindings, pattern filters, etc.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Routing\Router $router
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function boot(Router $router)
|
public function boot()
|
||||||
{
|
{
|
||||||
//
|
parent::boot();
|
||||||
|
|
||||||
parent::boot($router);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the routes for the application.
|
* Define the routes for the application.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Routing\Router $router
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function map(Router $router)
|
public function map()
|
||||||
{
|
{
|
||||||
$router->group(['namespace' => $this->namespace], function ($router) {
|
$this->mapWebRoutes();
|
||||||
require app_path('Http/routes.php');
|
// $this->mapApiRoutes();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Define the "web" routes for the application.
|
||||||
|
*
|
||||||
|
* These routes all receive session state, CSRF protection, etc.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function mapWebRoutes()
|
||||||
|
{
|
||||||
|
Route::group([
|
||||||
|
'middleware' => 'web',
|
||||||
|
'namespace' => $this->namespace,
|
||||||
|
], function ($router) {
|
||||||
|
require base_path('routes/web.php');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Define the "api" routes for the application.
|
||||||
|
*
|
||||||
|
* These routes are typically stateless.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function mapApiRoutes()
|
||||||
|
{
|
||||||
|
Route::group([
|
||||||
|
'middleware' => 'api',
|
||||||
|
'namespace' => $this->namespace,
|
||||||
|
'prefix' => 'api',
|
||||||
|
], function ($router) {
|
||||||
|
require base_path('routes/api.php');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,8 +132,8 @@ class BookRepo extends EntityRepo
|
||||||
{
|
{
|
||||||
$book = $this->book->newInstance($input);
|
$book = $this->book->newInstance($input);
|
||||||
$book->slug = $this->findSuitableSlug($book->name);
|
$book->slug = $this->findSuitableSlug($book->name);
|
||||||
$book->created_by = auth()->user()->id;
|
$book->created_by = user()->id;
|
||||||
$book->updated_by = auth()->user()->id;
|
$book->updated_by = user()->id;
|
||||||
$book->save();
|
$book->save();
|
||||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||||
return $book;
|
return $book;
|
||||||
|
@ -147,9 +147,11 @@ class BookRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function updateFromInput(Book $book, $input)
|
public function updateFromInput(Book $book, $input)
|
||||||
{
|
{
|
||||||
|
if ($book->name !== $input['name']) {
|
||||||
|
$book->slug = $this->findSuitableSlug($input['name'], $book->id);
|
||||||
|
}
|
||||||
$book->fill($input);
|
$book->fill($input);
|
||||||
$book->slug = $this->findSuitableSlug($book->name, $book->id);
|
$book->updated_by = user()->id;
|
||||||
$book->updated_by = auth()->user()->id;
|
|
||||||
$book->save();
|
$book->save();
|
||||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||||
return $book;
|
return $book;
|
||||||
|
@ -208,8 +210,7 @@ class BookRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function findSuitableSlug($name, $currentId = false)
|
public function findSuitableSlug($name, $currentId = false)
|
||||||
{
|
{
|
||||||
$slug = Str::slug($name);
|
$slug = $this->nameToSlug($name);
|
||||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
|
||||||
while ($this->doesSlugExist($slug, $currentId)) {
|
while ($this->doesSlugExist($slug, $currentId)) {
|
||||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,8 +98,8 @@ class ChapterRepo extends EntityRepo
|
||||||
{
|
{
|
||||||
$chapter = $this->chapter->newInstance($input);
|
$chapter = $this->chapter->newInstance($input);
|
||||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
|
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
|
||||||
$chapter->created_by = auth()->user()->id;
|
$chapter->created_by = user()->id;
|
||||||
$chapter->updated_by = auth()->user()->id;
|
$chapter->updated_by = user()->id;
|
||||||
$chapter = $book->chapters()->save($chapter);
|
$chapter = $book->chapters()->save($chapter);
|
||||||
$this->permissionService->buildJointPermissionsForEntity($chapter);
|
$this->permissionService->buildJointPermissionsForEntity($chapter);
|
||||||
return $chapter;
|
return $chapter;
|
||||||
|
@ -150,8 +150,7 @@ class ChapterRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||||
{
|
{
|
||||||
$slug = Str::slug($name);
|
$slug = $this->nameToSlug($name);
|
||||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
|
||||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,9 +132,8 @@ class EntityRepo
|
||||||
*/
|
*/
|
||||||
public function getUserDraftPages($count = 20, $page = 0)
|
public function getUserDraftPages($count = 20, $page = 0)
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
|
||||||
return $this->page->where('draft', '=', true)
|
return $this->page->where('draft', '=', true)
|
||||||
->where('created_by', '=', $user->id)
|
->where('created_by', '=', user()->id)
|
||||||
->orderBy('updated_at', 'desc')
|
->orderBy('updated_at', 'desc')
|
||||||
->skip($count * $page)->take($count)->get();
|
->skip($count * $page)->take($count)->get();
|
||||||
}
|
}
|
||||||
|
@ -270,6 +269,19 @@ class EntityRepo
|
||||||
$this->permissionService->buildJointPermissionsForEntities($collection);
|
$this->permissionService->buildJointPermissionsForEntities($collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a name as a url slug.
|
||||||
|
* @param $name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function nameToSlug($name)
|
||||||
|
{
|
||||||
|
$slug = str_replace(' ', '-', strtolower($name));
|
||||||
|
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
|
||||||
|
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ use BookStack\Image;
|
||||||
use BookStack\Page;
|
use BookStack\Page;
|
||||||
use BookStack\Services\ImageService;
|
use BookStack\Services\ImageService;
|
||||||
use BookStack\Services\PermissionService;
|
use BookStack\Services\PermissionService;
|
||||||
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||||
use Setting;
|
use Setting;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
@ -191,7 +192,12 @@ class ImageRepo
|
||||||
*/
|
*/
|
||||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||||
{
|
{
|
||||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
try {
|
||||||
|
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||||
|
} catch (FileNotFoundException $exception) {
|
||||||
|
$image->delete();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,10 @@ use BookStack\Book;
|
||||||
use BookStack\Chapter;
|
use BookStack\Chapter;
|
||||||
use BookStack\Entity;
|
use BookStack\Entity;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
|
use BookStack\Services\AttachmentService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
|
use DOMXPath;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use BookStack\Page;
|
use BookStack\Page;
|
||||||
use BookStack\PageRevision;
|
use BookStack\PageRevision;
|
||||||
|
@ -47,7 +49,7 @@ class PageRepo extends EntityRepo
|
||||||
* Get a page via a specific ID.
|
* Get a page via a specific ID.
|
||||||
* @param $id
|
* @param $id
|
||||||
* @param bool $allowDrafts
|
* @param bool $allowDrafts
|
||||||
* @return mixed
|
* @return Page
|
||||||
*/
|
*/
|
||||||
public function getById($id, $allowDrafts = false)
|
public function getById($id, $allowDrafts = false)
|
||||||
{
|
{
|
||||||
|
@ -58,7 +60,7 @@ class PageRepo extends EntityRepo
|
||||||
* Get a page identified by the given slug.
|
* Get a page identified by the given slug.
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @param $bookId
|
* @param $bookId
|
||||||
* @return mixed
|
* @return Page
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function getBySlug($slug, $bookId)
|
public function getBySlug($slug, $bookId)
|
||||||
|
@ -110,31 +112,6 @@ class PageRepo extends EntityRepo
|
||||||
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
|
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a new page into the system.
|
|
||||||
* Input validation must be done beforehand.
|
|
||||||
* @param array $input
|
|
||||||
* @param Book $book
|
|
||||||
* @param int $chapterId
|
|
||||||
* @return Page
|
|
||||||
*/
|
|
||||||
public function saveNew(array $input, Book $book, $chapterId = null)
|
|
||||||
{
|
|
||||||
$page = $this->newFromInput($input);
|
|
||||||
$page->slug = $this->findSuitableSlug($page->name, $book->id);
|
|
||||||
|
|
||||||
if ($chapterId) $page->chapter_id = $chapterId;
|
|
||||||
|
|
||||||
$page->html = $this->formatHtml($input['html']);
|
|
||||||
$page->text = strip_tags($page->html);
|
|
||||||
$page->created_by = auth()->user()->id;
|
|
||||||
$page->updated_by = auth()->user()->id;
|
|
||||||
|
|
||||||
$book->pages()->save($page);
|
|
||||||
return $page;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish a draft page to make it a normal page.
|
* Publish a draft page to make it a normal page.
|
||||||
* Sets the slug and updates the content.
|
* Sets the slug and updates the content.
|
||||||
|
@ -172,8 +149,8 @@ class PageRepo extends EntityRepo
|
||||||
{
|
{
|
||||||
$page = $this->page->newInstance();
|
$page = $this->page->newInstance();
|
||||||
$page->name = 'New Page';
|
$page->name = 'New Page';
|
||||||
$page->created_by = auth()->user()->id;
|
$page->created_by = user()->id;
|
||||||
$page->updated_by = auth()->user()->id;
|
$page->updated_by = user()->id;
|
||||||
$page->draft = true;
|
$page->draft = true;
|
||||||
|
|
||||||
if ($chapter) $page->chapter_id = $chapter->id;
|
if ($chapter) $page->chapter_id = $chapter->id;
|
||||||
|
@ -183,6 +160,35 @@ class PageRepo extends EntityRepo
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse te headers on the page to get a navigation menu
|
||||||
|
* @param Page $page
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getPageNav(Page $page)
|
||||||
|
{
|
||||||
|
if ($page->html == '') return null;
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
$doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xPath = new DOMXPath($doc);
|
||||||
|
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
|
||||||
|
|
||||||
|
if (is_null($headers)) return null;
|
||||||
|
|
||||||
|
$tree = [];
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
$text = $header->nodeValue;
|
||||||
|
$tree[] = [
|
||||||
|
'nodeName' => strtolower($header->nodeName),
|
||||||
|
'level' => intval(str_replace('h', '', $header->nodeName)),
|
||||||
|
'link' => '#' . $header->getAttribute('id'),
|
||||||
|
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $tree;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a page's html to be tagged correctly
|
* Formats a page's html to be tagged correctly
|
||||||
* within the system.
|
* within the system.
|
||||||
|
@ -325,7 +331,7 @@ class PageRepo extends EntityRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update with new details
|
// Update with new details
|
||||||
$userId = auth()->user()->id;
|
$userId = user()->id;
|
||||||
$page->fill($input);
|
$page->fill($input);
|
||||||
$page->html = $this->formatHtml($input['html']);
|
$page->html = $this->formatHtml($input['html']);
|
||||||
$page->text = strip_tags($page->html);
|
$page->text = strip_tags($page->html);
|
||||||
|
@ -358,7 +364,7 @@ class PageRepo extends EntityRepo
|
||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
|
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
|
||||||
$page->text = strip_tags($page->html);
|
$page->text = strip_tags($page->html);
|
||||||
$page->updated_by = auth()->user()->id;
|
$page->updated_by = user()->id;
|
||||||
$page->save();
|
$page->save();
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
@ -371,21 +377,23 @@ class PageRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function saveRevision(Page $page, $summary = null)
|
public function saveRevision(Page $page, $summary = null)
|
||||||
{
|
{
|
||||||
$revision = $this->pageRevision->fill($page->toArray());
|
$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->page_id = $page->id;
|
||||||
$revision->slug = $page->slug;
|
$revision->slug = $page->slug;
|
||||||
$revision->book_slug = $page->book->slug;
|
$revision->book_slug = $page->book->slug;
|
||||||
$revision->created_by = auth()->user()->id;
|
$revision->created_by = user()->id;
|
||||||
$revision->created_at = $page->updated_at;
|
$revision->created_at = $page->updated_at;
|
||||||
$revision->type = 'version';
|
$revision->type = 'version';
|
||||||
$revision->summary = $summary;
|
$revision->summary = $summary;
|
||||||
$revision->save();
|
$revision->save();
|
||||||
|
|
||||||
// Clear old revisions
|
// Clear old revisions
|
||||||
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
||||||
$this->pageRevision->where('page_id', '=', $page->id)
|
$this->pageRevision->where('page_id', '=', $page->id)
|
||||||
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
|
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $revision;
|
return $revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,7 +405,7 @@ class PageRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function saveUpdateDraft(Page $page, $data = [])
|
public function saveUpdateDraft(Page $page, $data = [])
|
||||||
{
|
{
|
||||||
$userId = auth()->user()->id;
|
$userId = user()->id;
|
||||||
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
|
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
|
||||||
|
|
||||||
if ($drafts->count() > 0) {
|
if ($drafts->count() > 0) {
|
||||||
|
@ -528,7 +536,7 @@ class PageRepo extends EntityRepo
|
||||||
$query = $this->pageRevision->where('type', '=', 'update_draft')
|
$query = $this->pageRevision->where('type', '=', 'update_draft')
|
||||||
->where('page_id', '=', $page->id)
|
->where('page_id', '=', $page->id)
|
||||||
->where('updated_at', '>', $page->updated_at)
|
->where('updated_at', '>', $page->updated_at)
|
||||||
->where('created_by', '!=', auth()->user()->id)
|
->where('created_by', '!=', user()->id)
|
||||||
->with('createdBy');
|
->with('createdBy');
|
||||||
|
|
||||||
if ($minRange !== null) {
|
if ($minRange !== null) {
|
||||||
|
@ -541,7 +549,7 @@ class PageRepo extends EntityRepo
|
||||||
/**
|
/**
|
||||||
* Gets a single revision via it's id.
|
* Gets a single revision via it's id.
|
||||||
* @param $id
|
* @param $id
|
||||||
* @return mixed
|
* @return PageRevision
|
||||||
*/
|
*/
|
||||||
public function getRevisionById($id)
|
public function getRevisionById($id)
|
||||||
{
|
{
|
||||||
|
@ -606,8 +614,7 @@ class PageRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||||
{
|
{
|
||||||
$slug = Str::slug($name);
|
$slug = $this->nameToSlug($name);
|
||||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
|
||||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||||
}
|
}
|
||||||
|
@ -626,12 +633,20 @@ class PageRepo extends EntityRepo
|
||||||
$page->revisions()->delete();
|
$page->revisions()->delete();
|
||||||
$page->permissions()->delete();
|
$page->permissions()->delete();
|
||||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||||
|
|
||||||
|
// Delete AttachedFiles
|
||||||
|
$attachmentService = app(AttachmentService::class);
|
||||||
|
foreach ($page->attachments as $attachment) {
|
||||||
|
$attachmentService->deleteFile($attachment);
|
||||||
|
}
|
||||||
|
|
||||||
$page->delete();
|
$page->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest pages added to the system.
|
* Get the latest pages added to the system.
|
||||||
* @param $count
|
* @param $count
|
||||||
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function getRecentlyCreatedPaginated($count = 20)
|
public function getRecentlyCreatedPaginated($count = 20)
|
||||||
{
|
{
|
||||||
|
@ -641,6 +656,7 @@ class PageRepo extends EntityRepo
|
||||||
/**
|
/**
|
||||||
* Get the latest pages added to the system.
|
* Get the latest pages added to the system.
|
||||||
* @param $count
|
* @param $count
|
||||||
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function getRecentlyUpdatedPaginated($count = 20)
|
public function getRecentlyUpdatedPaginated($count = 20)
|
||||||
{
|
{
|
||||||
|
|
|
@ -35,7 +35,7 @@ class PermissionsRepo
|
||||||
*/
|
*/
|
||||||
public function getAllRoles()
|
public function getAllRoles()
|
||||||
{
|
{
|
||||||
return $this->role->where('hidden', '=', false)->get();
|
return $this->role->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,7 +45,7 @@ class PermissionsRepo
|
||||||
*/
|
*/
|
||||||
public function getAllRolesExcept(Role $role)
|
public function getAllRolesExcept(Role $role)
|
||||||
{
|
{
|
||||||
return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
|
return $this->role->where('id', '!=', $role->id)->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,8 +90,6 @@ class PermissionsRepo
|
||||||
{
|
{
|
||||||
$role = $this->role->findOrFail($roleId);
|
$role = $this->role->findOrFail($roleId);
|
||||||
|
|
||||||
if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
|
|
||||||
|
|
||||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||||
$this->assignRolePermissions($role, $permissions);
|
$this->assignRolePermissions($role, $permissions);
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use BookStack\Role;
|
use BookStack\Role;
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
|
use Exception;
|
||||||
use Setting;
|
use Setting;
|
||||||
|
|
||||||
class UserRepo
|
class UserRepo
|
||||||
|
@ -84,9 +85,14 @@ class UserRepo
|
||||||
|
|
||||||
// Get avatar from gravatar and save
|
// Get avatar from gravatar and save
|
||||||
if (!config('services.disable_services')) {
|
if (!config('services.disable_services')) {
|
||||||
$avatar = \Images::saveUserGravatar($user);
|
try {
|
||||||
$user->avatar()->associate($avatar);
|
$avatar = \Images::saveUserGravatar($user);
|
||||||
$user->save();
|
$user->avatar()->associate($avatar);
|
||||||
|
$user->save();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$user->save();
|
||||||
|
\Log::error('Failed to save user gravatar image');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
|
@ -193,9 +199,9 @@ class UserRepo
|
||||||
* Get the roles in the system that are assignable to a user.
|
* Get the roles in the system that are assignable to a user.
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function getAssignableRoles()
|
public function getAllRoles()
|
||||||
{
|
{
|
||||||
return $this->role->visible();
|
return $this->role->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,7 +211,7 @@ class UserRepo
|
||||||
*/
|
*/
|
||||||
public function getRestrictableRoles()
|
public function getRestrictableRoles()
|
||||||
{
|
{
|
||||||
return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
|
return $this->role->where('system_name', '!=', 'admin')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -66,7 +66,7 @@ class Role extends Model
|
||||||
/**
|
/**
|
||||||
* Get the role object for the specified role.
|
* Get the role object for the specified role.
|
||||||
* @param $roleName
|
* @param $roleName
|
||||||
* @return mixed
|
* @return Role
|
||||||
*/
|
*/
|
||||||
public static function getRole($roleName)
|
public static function getRole($roleName)
|
||||||
{
|
{
|
||||||
|
@ -76,7 +76,7 @@ class Role extends Model
|
||||||
/**
|
/**
|
||||||
* Get the role object for the specified system role.
|
* Get the role object for the specified system role.
|
||||||
* @param $roleName
|
* @param $roleName
|
||||||
* @return mixed
|
* @return Role
|
||||||
*/
|
*/
|
||||||
public static function getSystemRole($roleName)
|
public static function getSystemRole($roleName)
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,7 +19,7 @@ class ActivityService
|
||||||
{
|
{
|
||||||
$this->activity = $activity;
|
$this->activity = $activity;
|
||||||
$this->permissionService = $permissionService;
|
$this->permissionService = $permissionService;
|
||||||
$this->user = auth()->user();
|
$this->user = user();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
<?php namespace BookStack\Services;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\FileUploadException;
|
||||||
|
use BookStack\Attachment;
|
||||||
|
use Exception;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
class AttachmentService extends UploadService
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an attachment from storage.
|
||||||
|
* @param Attachment $attachment
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAttachmentFromStorage(Attachment $attachment)
|
||||||
|
{
|
||||||
|
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
|
||||||
|
return $this->getStorage()->get($attachmentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new attachment upon user upload.
|
||||||
|
* @param UploadedFile $uploadedFile
|
||||||
|
* @param int $page_id
|
||||||
|
* @return Attachment
|
||||||
|
* @throws FileUploadException
|
||||||
|
*/
|
||||||
|
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
|
||||||
|
{
|
||||||
|
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||||
|
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
|
||||||
|
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||||
|
|
||||||
|
$attachment = Attachment::forceCreate([
|
||||||
|
'name' => $attachmentName,
|
||||||
|
'path' => $attachmentPath,
|
||||||
|
'extension' => $uploadedFile->getClientOriginalExtension(),
|
||||||
|
'uploaded_to' => $page_id,
|
||||||
|
'created_by' => user()->id,
|
||||||
|
'updated_by' => user()->id,
|
||||||
|
'order' => $largestExistingOrder + 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a upload, saving to a file and deleting any existing uploads
|
||||||
|
* attached to that file.
|
||||||
|
* @param UploadedFile $uploadedFile
|
||||||
|
* @param Attachment $attachment
|
||||||
|
* @return Attachment
|
||||||
|
* @throws FileUploadException
|
||||||
|
*/
|
||||||
|
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
|
||||||
|
{
|
||||||
|
if (!$attachment->external) {
|
||||||
|
$this->deleteFileInStorage($attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||||
|
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
|
||||||
|
|
||||||
|
$attachment->name = $attachmentName;
|
||||||
|
$attachment->path = $attachmentPath;
|
||||||
|
$attachment->external = false;
|
||||||
|
$attachment->extension = $uploadedFile->getClientOriginalExtension();
|
||||||
|
$attachment->save();
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a new File attachment from a given link and name.
|
||||||
|
* @param string $name
|
||||||
|
* @param string $link
|
||||||
|
* @param int $page_id
|
||||||
|
* @return Attachment
|
||||||
|
*/
|
||||||
|
public function saveNewFromLink($name, $link, $page_id)
|
||||||
|
{
|
||||||
|
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||||
|
return Attachment::forceCreate([
|
||||||
|
'name' => $name,
|
||||||
|
'path' => $link,
|
||||||
|
'external' => true,
|
||||||
|
'extension' => '',
|
||||||
|
'uploaded_to' => $page_id,
|
||||||
|
'created_by' => user()->id,
|
||||||
|
'updated_by' => user()->id,
|
||||||
|
'order' => $largestExistingOrder + 1
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param $pageId
|
||||||
|
*/
|
||||||
|
public function updateFileOrderWithinPage($attachmentList, $pageId)
|
||||||
|
{
|
||||||
|
foreach ($attachmentList as $index => $attachment) {
|
||||||
|
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the details of a file.
|
||||||
|
* @param Attachment $attachment
|
||||||
|
* @param $requestData
|
||||||
|
* @return Attachment
|
||||||
|
*/
|
||||||
|
public function updateFile(Attachment $attachment, $requestData)
|
||||||
|
{
|
||||||
|
$attachment->name = $requestData['name'];
|
||||||
|
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
|
||||||
|
$attachment->path = $requestData['link'];
|
||||||
|
if (!$attachment->external) {
|
||||||
|
$this->deleteFileInStorage($attachment);
|
||||||
|
$attachment->external = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$attachment->save();
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a File from the database and storage.
|
||||||
|
* @param Attachment $attachment
|
||||||
|
*/
|
||||||
|
public function deleteFile(Attachment $attachment)
|
||||||
|
{
|
||||||
|
if ($attachment->external) {
|
||||||
|
$attachment->delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->deleteFileInStorage($attachment);
|
||||||
|
$attachment->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file from the filesystem it sits on.
|
||||||
|
* Cleans any empty leftover folders.
|
||||||
|
* @param Attachment $attachment
|
||||||
|
*/
|
||||||
|
protected function deleteFileInStorage(Attachment $attachment)
|
||||||
|
{
|
||||||
|
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
|
||||||
|
$storage = $this->getStorage();
|
||||||
|
$dirPath = dirname($storedFilePath);
|
||||||
|
|
||||||
|
$storage->delete($storedFilePath);
|
||||||
|
if (count($storage->allFiles($dirPath)) === 0) {
|
||||||
|
$storage->deleteDirectory($dirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a file in storage with the given filename
|
||||||
|
* @param $attachmentName
|
||||||
|
* @param UploadedFile $uploadedFile
|
||||||
|
* @return string
|
||||||
|
* @throws FileUploadException
|
||||||
|
*/
|
||||||
|
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
|
||||||
|
{
|
||||||
|
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||||
|
|
||||||
|
$storage = $this->getStorage();
|
||||||
|
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
|
||||||
|
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
|
||||||
|
|
||||||
|
$uploadFileName = $attachmentName;
|
||||||
|
while ($storage->exists($storageBasePath . $uploadFileName)) {
|
||||||
|
$uploadFileName = str_random(3) . $uploadFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentPath = $attachmentBasePath . $uploadFileName;
|
||||||
|
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$storage->put($attachmentStoragePath, $attachmentData);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new FileUploadException('File path ' . $attachmentStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
|
||||||
|
}
|
||||||
|
return $attachmentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,30 +1,27 @@
|
||||||
<?php namespace BookStack\Services;
|
<?php namespace BookStack\Services;
|
||||||
|
|
||||||
|
use BookStack\Notifications\ConfirmEmail;
|
||||||
|
use BookStack\Repos\UserRepo;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Contracts\Mail\Mailer;
|
|
||||||
use Illuminate\Mail\Message;
|
|
||||||
use BookStack\EmailConfirmation;
|
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Repos\UserRepo;
|
|
||||||
use BookStack\Setting;
|
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
|
use Illuminate\Database\Connection as Database;
|
||||||
|
|
||||||
class EmailConfirmationService
|
class EmailConfirmationService
|
||||||
{
|
{
|
||||||
protected $mailer;
|
protected $db;
|
||||||
protected $emailConfirmation;
|
protected $users;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EmailConfirmationService constructor.
|
* EmailConfirmationService constructor.
|
||||||
* @param Mailer $mailer
|
* @param Database $db
|
||||||
* @param EmailConfirmation $emailConfirmation
|
* @param UserRepo $users
|
||||||
*/
|
*/
|
||||||
public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation)
|
public function __construct(Database $db, UserRepo $users)
|
||||||
{
|
{
|
||||||
$this->mailer = $mailer;
|
$this->db = $db;
|
||||||
$this->emailConfirmation = $emailConfirmation;
|
$this->users = $users;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,16 +35,28 @@ class EmailConfirmationService
|
||||||
if ($user->email_confirmed) {
|
if ($user->email_confirmed) {
|
||||||
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
|
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->deleteConfirmationsByUser($user);
|
$this->deleteConfirmationsByUser($user);
|
||||||
|
$token = $this->createEmailConfirmation($user);
|
||||||
|
|
||||||
|
$user->notify(new ConfirmEmail($token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new email confirmation in the database and returns the token.
|
||||||
|
* @param User $user
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function createEmailConfirmation(User $user)
|
||||||
|
{
|
||||||
$token = $this->getToken();
|
$token = $this->getToken();
|
||||||
$this->emailConfirmation->create([
|
$this->db->table('email_confirmations')->insert([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'updated_at' => Carbon::now()
|
||||||
]);
|
]);
|
||||||
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
|
return $token;
|
||||||
$appName = setting('app-name', 'BookStack');
|
|
||||||
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,22 +68,24 @@ class EmailConfirmationService
|
||||||
*/
|
*/
|
||||||
public function getEmailConfirmationFromToken($token)
|
public function getEmailConfirmationFromToken($token)
|
||||||
{
|
{
|
||||||
$emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first();
|
$emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
|
||||||
// If not found
|
|
||||||
|
// If not found show error
|
||||||
if ($emailConfirmation === null) {
|
if ($emailConfirmation === null) {
|
||||||
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
|
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If more than a day old
|
// If more than a day old
|
||||||
if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) {
|
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
|
||||||
$this->sendConfirmation($emailConfirmation->user);
|
$user = $this->users->getById($emailConfirmation->user_id);
|
||||||
|
$this->sendConfirmation($user);
|
||||||
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
|
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
|
||||||
return $emailConfirmation;
|
return $emailConfirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all email confirmations that belong to a user.
|
* Delete all email confirmations that belong to a user.
|
||||||
* @param User $user
|
* @param User $user
|
||||||
|
@ -82,7 +93,7 @@ class EmailConfirmationService
|
||||||
*/
|
*/
|
||||||
public function deleteConfirmationsByUser(User $user)
|
public function deleteConfirmationsByUser(User $user)
|
||||||
{
|
{
|
||||||
return $this->emailConfirmation->where('user_id', '=', $user->id)->delete();
|
return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,7 +103,7 @@ class EmailConfirmationService
|
||||||
protected function getToken()
|
protected function getToken()
|
||||||
{
|
{
|
||||||
$token = str_random(24);
|
$token = str_random(24);
|
||||||
while ($this->emailConfirmation->where('token', '=', $token)->exists()) {
|
while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
|
||||||
$token = str_random(25);
|
$token = str_random(25);
|
||||||
}
|
}
|
||||||
return $token;
|
return $token;
|
||||||
|
|
|
@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
|
||||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||||
use Setting;
|
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
class ImageService
|
class ImageService extends UploadService
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $imageTool;
|
protected $imageTool;
|
||||||
protected $fileSystem;
|
|
||||||
protected $cache;
|
protected $cache;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var FileSystemInstance
|
|
||||||
*/
|
|
||||||
protected $storageInstance;
|
|
||||||
protected $storageUrl;
|
protected $storageUrl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,8 +27,8 @@ class ImageService
|
||||||
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
|
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
|
||||||
{
|
{
|
||||||
$this->imageTool = $imageTool;
|
$this->imageTool = $imageTool;
|
||||||
$this->fileSystem = $fileSystem;
|
|
||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
|
parent::__construct($fileSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +81,9 @@ class ImageService
|
||||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||||
|
|
||||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
||||||
|
|
||||||
|
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
|
||||||
|
|
||||||
while ($storage->exists($imagePath . $imageName)) {
|
while ($storage->exists($imagePath . $imageName)) {
|
||||||
$imageName = str_random(3) . $imageName;
|
$imageName = str_random(3) . $imageName;
|
||||||
}
|
}
|
||||||
|
@ -100,6 +96,8 @@ class ImageService
|
||||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
|
||||||
|
|
||||||
$imageDetails = [
|
$imageDetails = [
|
||||||
'name' => $imageName,
|
'name' => $imageName,
|
||||||
'path' => $fullPath,
|
'path' => $fullPath,
|
||||||
|
@ -108,8 +106,8 @@ class ImageService
|
||||||
'uploaded_to' => $uploadedTo
|
'uploaded_to' => $uploadedTo
|
||||||
];
|
];
|
||||||
|
|
||||||
if (auth()->user() && auth()->user()->id !== 0) {
|
if (user()->id !== 0) {
|
||||||
$userId = auth()->user()->id;
|
$userId = user()->id;
|
||||||
$imageDetails['created_by'] = $userId;
|
$imageDetails['created_by'] = $userId;
|
||||||
$imageDetails['updated_by'] = $userId;
|
$imageDetails['updated_by'] = $userId;
|
||||||
}
|
}
|
||||||
|
@ -119,6 +117,16 @@ class ImageService
|
||||||
return $image;
|
return $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the storage path, Dependant of storage type.
|
||||||
|
* @param Image $image
|
||||||
|
* @return mixed|string
|
||||||
|
*/
|
||||||
|
protected function getPath(Image $image)
|
||||||
|
{
|
||||||
|
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the thumbnail for an image.
|
* Get the thumbnail for an image.
|
||||||
* If $keepRatio is true only the width will be used.
|
* If $keepRatio is true only the width will be used.
|
||||||
|
@ -135,7 +143,8 @@ class ImageService
|
||||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||||
{
|
{
|
||||||
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
|
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
|
||||||
$thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
|
$imagePath = $this->getPath($image);
|
||||||
|
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
|
||||||
|
|
||||||
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
|
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
|
||||||
return $this->getPublicUrl($thumbFilePath);
|
return $this->getPublicUrl($thumbFilePath);
|
||||||
|
@ -148,7 +157,7 @@ class ImageService
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
$thumb = $this->imageTool->make($storage->get($imagePath));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||||
|
@ -183,8 +192,8 @@ class ImageService
|
||||||
{
|
{
|
||||||
$storage = $this->getStorage();
|
$storage = $this->getStorage();
|
||||||
|
|
||||||
$imageFolder = dirname($image->path);
|
$imageFolder = dirname($this->getPath($image));
|
||||||
$imageFileName = basename($image->path);
|
$imageFileName = basename($this->getPath($image));
|
||||||
$allImages = collect($storage->allFiles($imageFolder));
|
$allImages = collect($storage->allFiles($imageFolder));
|
||||||
|
|
||||||
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
|
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
|
||||||
|
@ -213,7 +222,7 @@ class ImageService
|
||||||
public function saveUserGravatar(User $user, $size = 500)
|
public function saveUserGravatar(User $user, $size = 500)
|
||||||
{
|
{
|
||||||
$emailHash = md5(strtolower(trim($user->email)));
|
$emailHash = md5(strtolower(trim($user->email)));
|
||||||
$url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||||
$image->created_by = $user->id;
|
$image->created_by = $user->id;
|
||||||
|
@ -222,35 +231,9 @@ class ImageService
|
||||||
return $image;
|
return $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the storage that will be used for storing images.
|
|
||||||
* @return FileSystemInstance
|
|
||||||
*/
|
|
||||||
private function getStorage()
|
|
||||||
{
|
|
||||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
|
||||||
|
|
||||||
$storageType = config('filesystems.default');
|
|
||||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
|
||||||
|
|
||||||
return $this->storageInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether or not a folder is empty.
|
|
||||||
* @param $path
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
private function isFolderEmpty($path)
|
|
||||||
{
|
|
||||||
$files = $this->getStorage()->files($path);
|
|
||||||
$folders = $this->getStorage()->directories($path);
|
|
||||||
return count($files) === 0 && count($folders) === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a public facing url for an image by checking relevant environment variables.
|
* Gets a public facing url for an image by checking relevant environment variables.
|
||||||
* @param $filePath
|
* @param string $filePath
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private function getPublicUrl($filePath)
|
private function getPublicUrl($filePath)
|
||||||
|
@ -273,6 +256,8 @@ class ImageService
|
||||||
$this->storageUrl = $storageUrl;
|
$this->storageUrl = $storageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
|
||||||
|
|
||||||
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,15 @@ use BookStack\Page;
|
||||||
use BookStack\Role;
|
use BookStack\Role;
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class PermissionService
|
class PermissionService
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $userRoles;
|
|
||||||
protected $isAdmin;
|
|
||||||
protected $currentAction;
|
protected $currentAction;
|
||||||
protected $currentUser;
|
protected $isAdminUser;
|
||||||
|
protected $userRoles = false;
|
||||||
|
protected $currentUserModel = false;
|
||||||
|
|
||||||
public $book;
|
public $book;
|
||||||
public $chapter;
|
public $chapter;
|
||||||
|
@ -37,12 +38,6 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
|
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||||
{
|
{
|
||||||
$this->currentUser = auth()->user();
|
|
||||||
$userSet = $this->currentUser !== null;
|
|
||||||
$this->userRoles = false;
|
|
||||||
$this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
|
|
||||||
if (!$userSet) $this->currentUser = new User();
|
|
||||||
|
|
||||||
$this->jointPermission = $jointPermission;
|
$this->jointPermission = $jointPermission;
|
||||||
$this->role = $role;
|
$this->role = $role;
|
||||||
$this->book = $book;
|
$this->book = $book;
|
||||||
|
@ -117,7 +112,7 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
foreach ($this->currentUser->roles as $role) {
|
foreach ($this->currentUser()->roles as $role) {
|
||||||
$roles[] = $role->id;
|
$roles[] = $role->id;
|
||||||
}
|
}
|
||||||
return $roles;
|
return $roles;
|
||||||
|
@ -389,7 +384,11 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||||
{
|
{
|
||||||
if ($this->isAdmin) return true;
|
if ($this->isAdmin()) {
|
||||||
|
$this->clean();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$explodedPermission = explode('-', $permission);
|
$explodedPermission = explode('-', $permission);
|
||||||
|
|
||||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||||
|
@ -400,10 +399,10 @@ class PermissionService
|
||||||
|
|
||||||
// Handle non entity specific jointPermissions
|
// Handle non entity specific jointPermissions
|
||||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||||
$allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
|
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
|
||||||
$ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
|
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
|
||||||
$this->currentAction = 'view';
|
$this->currentAction = 'view';
|
||||||
$isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
|
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
|
||||||
return ($allPermission || ($isOwner && $ownPermission));
|
return ($allPermission || ($isOwner && $ownPermission));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +412,9 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||||
|
$this->clean();
|
||||||
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -443,7 +444,7 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
protected function entityRestrictionQuery($query)
|
protected function entityRestrictionQuery($query)
|
||||||
{
|
{
|
||||||
return $query->where(function ($parentQuery) {
|
$q = $query->where(function ($parentQuery) {
|
||||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
|
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
|
||||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||||
->where('action', '=', $this->currentAction)
|
->where('action', '=', $this->currentAction)
|
||||||
|
@ -451,11 +452,13 @@ class PermissionService
|
||||||
$query->where('has_permission', '=', true)
|
$query->where('has_permission', '=', true)
|
||||||
->orWhere(function ($query) {
|
->orWhere(function ($query) {
|
||||||
$query->where('has_permission_own', '=', true)
|
$query->where('has_permission_own', '=', true)
|
||||||
->where('created_by', '=', $this->currentUser->id);
|
->where('created_by', '=', $this->currentUser()->id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
$this->clean();
|
||||||
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -469,9 +472,9 @@ class PermissionService
|
||||||
// Prevent drafts being visible to others.
|
// Prevent drafts being visible to others.
|
||||||
$query = $query->where(function ($query) {
|
$query = $query->where(function ($query) {
|
||||||
$query->where('draft', '=', false);
|
$query->where('draft', '=', false);
|
||||||
if ($this->currentUser) {
|
if ($this->currentUser()) {
|
||||||
$query->orWhere(function ($query) {
|
$query->orWhere(function ($query) {
|
||||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
|
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -509,7 +512,10 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
public function enforceEntityRestrictions($query, $action = 'view')
|
public function enforceEntityRestrictions($query, $action = 'view')
|
||||||
{
|
{
|
||||||
if ($this->isAdmin) return $query;
|
if ($this->isAdmin()) {
|
||||||
|
$this->clean();
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
$this->currentAction = $action;
|
$this->currentAction = $action;
|
||||||
return $this->entityRestrictionQuery($query);
|
return $this->entityRestrictionQuery($query);
|
||||||
}
|
}
|
||||||
|
@ -524,11 +530,15 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||||
{
|
{
|
||||||
if ($this->isAdmin) return $query;
|
if ($this->isAdmin()) {
|
||||||
|
$this->clean();
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
$this->currentAction = 'view';
|
$this->currentAction = 'view';
|
||||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||||
|
|
||||||
return $query->where(function ($query) use ($tableDetails) {
|
$q = $query->where(function ($query) use ($tableDetails) {
|
||||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||||
$permissionQuery->select('id')->from('joint_permissions')
|
$permissionQuery->select('id')->from('joint_permissions')
|
||||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||||
|
@ -538,12 +548,12 @@ class PermissionService
|
||||||
->where(function ($query) {
|
->where(function ($query) {
|
||||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||||
$query->where('has_permission_own', '=', true)
|
$query->where('has_permission_own', '=', true)
|
||||||
->where('created_by', '=', $this->currentUser->id);
|
->where('created_by', '=', $this->currentUser()->id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -555,11 +565,15 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||||
{
|
{
|
||||||
if ($this->isAdmin) return $query;
|
if ($this->isAdmin()) {
|
||||||
|
$this->clean();
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
$this->currentAction = 'view';
|
$this->currentAction = 'view';
|
||||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||||
|
|
||||||
return $query->where(function ($query) use ($tableDetails) {
|
$q = $query->where(function ($query) use ($tableDetails) {
|
||||||
$query->where(function ($query) use (&$tableDetails) {
|
$query->where(function ($query) use (&$tableDetails) {
|
||||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||||
$permissionQuery->select('id')->from('joint_permissions')
|
$permissionQuery->select('id')->from('joint_permissions')
|
||||||
|
@ -570,12 +584,50 @@ class PermissionService
|
||||||
->where(function ($query) {
|
->where(function ($query) {
|
||||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||||
$query->where('has_permission_own', '=', true)
|
$query->where('has_permission_own', '=', true)
|
||||||
->where('created_by', '=', $this->currentUser->id);
|
->where('created_by', '=', $this->currentUser()->id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||||
});
|
});
|
||||||
|
$this->clean();
|
||||||
|
return $q;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current user is an admin.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isAdmin()
|
||||||
|
{
|
||||||
|
if ($this->isAdminUser === null) {
|
||||||
|
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isAdminUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current user
|
||||||
|
* @return User
|
||||||
|
*/
|
||||||
|
private function currentUser()
|
||||||
|
{
|
||||||
|
if ($this->currentUserModel === false) {
|
||||||
|
$this->currentUserModel = user();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->currentUserModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean the cached user elements.
|
||||||
|
*/
|
||||||
|
private function clean()
|
||||||
|
{
|
||||||
|
$this->currentUserModel = false;
|
||||||
|
$this->userRoles = false;
|
||||||
|
$this->isAdminUser = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -100,7 +100,7 @@ class SocialAuthService
|
||||||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||||
$user = $this->userRepo->getByEmail($socialUser->getEmail());
|
$user = $this->userRepo->getByEmail($socialUser->getEmail());
|
||||||
$isLoggedIn = auth()->check();
|
$isLoggedIn = auth()->check();
|
||||||
$currentUser = auth()->user();
|
$currentUser = user();
|
||||||
|
|
||||||
// When a user is not logged in and a matching SocialAccount exists,
|
// When a user is not logged in and a matching SocialAccount exists,
|
||||||
// Simply log the user into the application.
|
// Simply log the user into the application.
|
||||||
|
@ -214,9 +214,9 @@ class SocialAuthService
|
||||||
public function detachSocialAccount($socialDriver)
|
public function detachSocialAccount($socialDriver)
|
||||||
{
|
{
|
||||||
session();
|
session();
|
||||||
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||||
session()->flash('success', title_case($socialDriver) . ' account successfully detached');
|
session()->flash('success', title_case($socialDriver) . ' account successfully detached');
|
||||||
return redirect(auth()->user()->getEditUrl());
|
return redirect(user()->getEditUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php namespace BookStack\Services;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||||
|
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||||
|
|
||||||
|
class UploadService
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var FileSystem
|
||||||
|
*/
|
||||||
|
protected $fileSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var FileSystemInstance
|
||||||
|
*/
|
||||||
|
protected $storageInstance;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileService constructor.
|
||||||
|
* @param $fileSystem
|
||||||
|
*/
|
||||||
|
public function __construct(FileSystem $fileSystem)
|
||||||
|
{
|
||||||
|
$this->fileSystem = $fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the storage that will be used for storing images.
|
||||||
|
* @return FileSystemInstance
|
||||||
|
*/
|
||||||
|
protected function getStorage()
|
||||||
|
{
|
||||||
|
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||||
|
|
||||||
|
$storageType = config('filesystems.default');
|
||||||
|
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||||
|
|
||||||
|
return $this->storageInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not a folder is empty.
|
||||||
|
* @param $path
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isFolderEmpty($path)
|
||||||
|
{
|
||||||
|
$files = $this->getStorage()->files($path);
|
||||||
|
$folders = $this->getStorage()->directories($path);
|
||||||
|
return (count($files) === 0 && count($folders) === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if using a local filesystem.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isLocal()
|
||||||
|
{
|
||||||
|
return strtolower(config('filesystems.default')) === 'local';
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ class ViewService
|
||||||
public function __construct(View $view, PermissionService $permissionService)
|
public function __construct(View $view, PermissionService $permissionService)
|
||||||
{
|
{
|
||||||
$this->view = $view;
|
$this->view = $view;
|
||||||
$this->user = auth()->user();
|
$this->user = user();
|
||||||
$this->permissionService = $permissionService;
|
$this->permissionService = $permissionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class ViewService
|
||||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
->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', '=', auth()->user()->id);
|
$query = $query->where('user_id', '=', user()->id);
|
||||||
|
|
||||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||||
|
|
34
app/User.php
34
app/User.php
|
@ -1,13 +1,16 @@
|
||||||
<?php namespace BookStack;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
|
use BookStack\Notifications\ResetPassword;
|
||||||
use Illuminate\Auth\Authenticatable;
|
use Illuminate\Auth\Authenticatable;
|
||||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||||
{
|
{
|
||||||
use Authenticatable, CanResetPassword;
|
use Authenticatable, CanResetPassword, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The database table used by the model.
|
* The database table used by the model.
|
||||||
|
@ -34,21 +37,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
protected $permissions;
|
protected $permissions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a default guest user.
|
* Returns the default public user.
|
||||||
|
* @return User
|
||||||
*/
|
*/
|
||||||
public static function getDefault()
|
public static function getDefault()
|
||||||
{
|
{
|
||||||
return new static([
|
return static::where('system_name', '=', 'public')->first();
|
||||||
'email' => 'guest',
|
}
|
||||||
'name' => 'Guest'
|
|
||||||
]);
|
/**
|
||||||
|
* Check if the user is the default public user.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isDefault()
|
||||||
|
{
|
||||||
|
return $this->system_name === 'public';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The roles that belong to the user.
|
* The roles that belong to the user.
|
||||||
|
* @return BelongsToMany
|
||||||
*/
|
*/
|
||||||
public function roles()
|
public function roles()
|
||||||
{
|
{
|
||||||
|
if ($this->id === 0) return ;
|
||||||
return $this->belongsToMany(Role::class);
|
return $this->belongsToMany(Role::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,4 +195,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the password reset notification.
|
||||||
|
* @param string $token
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function sendPasswordResetNotification($token)
|
||||||
|
{
|
||||||
|
$this->notify(new ResetPassword($token));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,29 +11,30 @@ use BookStack\Ownable;
|
||||||
*/
|
*/
|
||||||
function versioned_asset($file = '')
|
function versioned_asset($file = '')
|
||||||
{
|
{
|
||||||
// Don't require css and JS assets for testing
|
static $version = null;
|
||||||
if (config('app.env') === 'testing') return '';
|
|
||||||
|
|
||||||
static $manifest = null;
|
if (is_null($version)) {
|
||||||
$manifestPath = 'build/manifest.json';
|
$versionFile = base_path('version');
|
||||||
|
$version = trim(file_get_contents($versionFile));
|
||||||
if (is_null($manifest) && file_exists($manifestPath)) {
|
|
||||||
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
|
|
||||||
} else if (!file_exists($manifestPath)) {
|
|
||||||
if (config('app.env') !== 'production') {
|
|
||||||
$path = public_path($manifestPath);
|
|
||||||
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
|
|
||||||
} else {
|
|
||||||
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($manifest[$file])) {
|
$additional = '';
|
||||||
return baseUrl($manifest[$file]);
|
if (config('app.env') === 'development') {
|
||||||
|
$additional = sha1_file(public_path($file));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
$path = $file . '?version=' . urlencode($version) . $additional;
|
||||||
|
return baseUrl($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get the current User.
|
||||||
|
* Defaults to public 'Guest' user if not logged in.
|
||||||
|
* @return \BookStack\User
|
||||||
|
*/
|
||||||
|
function user()
|
||||||
|
{
|
||||||
|
return auth()->user() ?: \BookStack\User::getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +48,7 @@ function versioned_asset($file = '')
|
||||||
function userCan($permission, Ownable $ownable = null)
|
function userCan($permission, Ownable $ownable = null)
|
||||||
{
|
{
|
||||||
if ($ownable === null) {
|
if ($ownable === null) {
|
||||||
return auth()->user() && auth()->user()->can($permission);
|
return user() && user()->can($permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permission on ownable item
|
// Check permission on ownable item
|
||||||
|
@ -63,7 +64,7 @@ function userCan($permission, Ownable $ownable = null)
|
||||||
*/
|
*/
|
||||||
function setting($key, $default = false)
|
function setting($key, $default = false)
|
||||||
{
|
{
|
||||||
$settingService = app('BookStack\Services\SettingService');
|
$settingService = app(\BookStack\Services\SettingService::class);
|
||||||
return $settingService->get($key, $default);
|
return $settingService->get($key, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +80,7 @@ function baseUrl($path, $forceAppDomain = false)
|
||||||
if ($isFullUrl && !$forceAppDomain) return $path;
|
if ($isFullUrl && !$forceAppDomain) return $path;
|
||||||
$path = trim($path, '/');
|
$path = trim($path, '/');
|
||||||
|
|
||||||
|
// Remove non-specified domain if forced and we have a domain
|
||||||
if ($isFullUrl && $forceAppDomain) {
|
if ($isFullUrl && $forceAppDomain) {
|
||||||
$explodedPath = explode('/', $path);
|
$explodedPath = explode('/', $path);
|
||||||
$path = implode('/', array_splice($explodedPath, 3));
|
$path = implode('/', array_splice($explodedPath, 3));
|
||||||
|
@ -127,14 +129,14 @@ function sortUrl($path, $data, $overrideData = [])
|
||||||
{
|
{
|
||||||
$queryStringSections = [];
|
$queryStringSections = [];
|
||||||
$queryData = array_merge($data, $overrideData);
|
$queryData = array_merge($data, $overrideData);
|
||||||
|
|
||||||
// Change sorting direction is already sorted on current attribute
|
// Change sorting direction is already sorted on current attribute
|
||||||
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
||||||
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
||||||
} else {
|
} else {
|
||||||
$queryData['order'] = 'asc';
|
$queryData['order'] = 'asc';
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($queryData as $name => $value) {
|
foreach ($queryData as $name => $value) {
|
||||||
$trimmedVal = trim($value);
|
$trimmedVal = trim($value);
|
||||||
if ($trimmedVal === '') continue;
|
if ($trimmedVal === '') continue;
|
||||||
|
@ -144,4 +146,4 @@ function sortUrl($path, $data, $overrideData = [])
|
||||||
if (count($queryStringSections) === 0) return $path;
|
if (count($queryStringSections) === 0) return $path;
|
||||||
|
|
||||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,24 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.5.9",
|
"php": ">=5.6.4",
|
||||||
"laravel/framework": "5.2.*",
|
"laravel/framework": "^5.3.4",
|
||||||
|
"ext-tidy": "*",
|
||||||
"intervention/image": "^2.3",
|
"intervention/image": "^2.3",
|
||||||
"laravel/socialite": "^2.0",
|
"laravel/socialite": "^2.0",
|
||||||
"barryvdh/laravel-ide-helper": "^2.1",
|
"barryvdh/laravel-ide-helper": "^2.1",
|
||||||
"barryvdh/laravel-debugbar": "^2.0",
|
"barryvdh/laravel-debugbar": "^2.2.3",
|
||||||
"league/flysystem-aws-s3-v3": "^1.0",
|
"league/flysystem-aws-s3-v3": "^1.0",
|
||||||
"barryvdh/laravel-dompdf": "0.6.*",
|
"barryvdh/laravel-dompdf": "^0.7",
|
||||||
"predis/predis": "^1.0"
|
"predis/predis": "^1.1",
|
||||||
|
"gathercontent/htmldiff": "^0.2.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fzaninotto/faker": "~1.4",
|
"fzaninotto/faker": "~1.4",
|
||||||
"mockery/mockery": "0.9.*",
|
"mockery/mockery": "0.9.*",
|
||||||
"phpunit/phpunit": "~4.0",
|
"phpunit/phpunit": "~5.0",
|
||||||
"phpspec/phpspec": "~2.1",
|
"symfony/css-selector": "3.1.*",
|
||||||
"symfony/dom-crawler": "~3.0",
|
"symfony/dom-crawler": "3.1.*"
|
||||||
"symfony/css-selector": "~3.0"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
@ -37,21 +38,19 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"post-install-cmd": [
|
|
||||||
"php artisan clear-compiled",
|
|
||||||
"php artisan optimize"
|
|
||||||
],
|
|
||||||
"pre-update-cmd": [
|
|
||||||
"php artisan clear-compiled"
|
|
||||||
],
|
|
||||||
"post-update-cmd": [
|
|
||||||
"php artisan optimize"
|
|
||||||
],
|
|
||||||
"post-root-package-install": [
|
"post-root-package-install": [
|
||||||
"php -r \"copy('.env.example', '.env');\""
|
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
],
|
],
|
||||||
"post-create-project-cmd": [
|
"post-create-project-cmd": [
|
||||||
"php artisan key:generate"
|
"php artisan key:generate"
|
||||||
|
],
|
||||||
|
"post-install-cmd": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postInstall",
|
||||||
|
"php artisan optimize"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
|
||||||
|
"php artisan optimize"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -57,7 +57,7 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'locale' => 'en',
|
'locale' => env('APP_LANG', 'en'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -138,6 +138,7 @@ return [
|
||||||
Illuminate\Translation\TranslationServiceProvider::class,
|
Illuminate\Translation\TranslationServiceProvider::class,
|
||||||
Illuminate\Validation\ValidationServiceProvider::class,
|
Illuminate\Validation\ValidationServiceProvider::class,
|
||||||
Illuminate\View\ViewServiceProvider::class,
|
Illuminate\View\ViewServiceProvider::class,
|
||||||
|
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||||
Laravel\Socialite\SocialiteServiceProvider::class,
|
Laravel\Socialite\SocialiteServiceProvider::class,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -156,6 +157,7 @@ return [
|
||||||
|
|
||||||
BookStack\Providers\AuthServiceProvider::class,
|
BookStack\Providers\AuthServiceProvider::class,
|
||||||
BookStack\Providers\AppServiceProvider::class,
|
BookStack\Providers\AppServiceProvider::class,
|
||||||
|
BookStack\Providers\BroadcastServiceProvider::class,
|
||||||
BookStack\Providers\EventServiceProvider::class,
|
BookStack\Providers\EventServiceProvider::class,
|
||||||
BookStack\Providers\RouteServiceProvider::class,
|
BookStack\Providers\RouteServiceProvider::class,
|
||||||
BookStack\Providers\CustomFacadeProvider::class,
|
BookStack\Providers\CustomFacadeProvider::class,
|
||||||
|
@ -194,6 +196,7 @@ return [
|
||||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||||
'Log' => Illuminate\Support\Facades\Log::class,
|
'Log' => Illuminate\Support\Facades\Log::class,
|
||||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||||
|
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||||
'Password' => Illuminate\Support\Facades\Password::class,
|
'Password' => Illuminate\Support\Facades\Password::class,
|
||||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||||
|
|
|
@ -56,7 +56,7 @@ return [
|
||||||
|
|
||||||
'local' => [
|
'local' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => public_path(),
|
'root' => base_path(),
|
||||||
],
|
],
|
||||||
|
|
||||||
'ftp' => [
|
'ftp' => [
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'app-name' => 'BookStack',
|
'app-name' => 'BookStack',
|
||||||
|
'app-name-header' => true,
|
||||||
'app-editor' => 'wysiwyg',
|
'app-editor' => 'wysiwyg',
|
||||||
'app-color' => '#0288D1',
|
'app-color' => '#0288D1',
|
||||||
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
|
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
|
||||||
|
|
|
@ -129,7 +129,7 @@ class AddRolesAndPermissions extends Migration
|
||||||
|
|
||||||
// Set all current users as admins
|
// Set all current users as admins
|
||||||
// (At this point only the initially create user should be an admin)
|
// (At this point only the initially create user should be an admin)
|
||||||
$users = DB::table('users')->get();
|
$users = DB::table('users')->get()->all();
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
DB::table('role_user')->insert([
|
DB::table('role_user')->insert([
|
||||||
'role_id' => $adminId,
|
'role_id' => $adminId,
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class RemoveHiddenRoles extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
// Remove the hidden property from roles
|
||||||
|
Schema::table('roles', function(Blueprint $table) {
|
||||||
|
$table->dropColumn('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add column to mark system users
|
||||||
|
Schema::table('users', function(Blueprint $table) {
|
||||||
|
$table->string('system_name')->nullable()->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert our new public system user.
|
||||||
|
$publicUserId = DB::table('users')->insertGetId([
|
||||||
|
'email' => 'guest@example.com',
|
||||||
|
'name' => 'Guest',
|
||||||
|
'system_name' => 'public',
|
||||||
|
'email_confirmed' => true,
|
||||||
|
'created_at' => \Carbon\Carbon::now(),
|
||||||
|
'updated_at' => \Carbon\Carbon::now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get the public role
|
||||||
|
$publicRole = DB::table('roles')->where('system_name', '=', 'public')->first();
|
||||||
|
|
||||||
|
// Connect the new public user to the public role
|
||||||
|
DB::table('role_user')->insert([
|
||||||
|
'user_id' => $publicUserId,
|
||||||
|
'role_id' => $publicRole->id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('roles', function(Blueprint $table) {
|
||||||
|
$table->boolean('hidden')->default(false);
|
||||||
|
$table->index('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('users')->where('system_name', '=', 'public')->delete();
|
||||||
|
|
||||||
|
Schema::table('users', function(Blueprint $table) {
|
||||||
|
$table->dropColumn('system_name');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('roles')->where('system_name', '=', 'public')->update(['hidden' => true]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateAttachmentsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('attachments', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('path');
|
||||||
|
$table->string('extension', 20);
|
||||||
|
$table->integer('uploaded_to');
|
||||||
|
|
||||||
|
$table->boolean('external');
|
||||||
|
$table->integer('order');
|
||||||
|
|
||||||
|
$table->integer('created_by');
|
||||||
|
$table->integer('updated_by');
|
||||||
|
|
||||||
|
$table->index('uploaded_to');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get roles with permissions we need to change
|
||||||
|
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
|
||||||
|
|
||||||
|
// Create & attach new entity permissions
|
||||||
|
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||||
|
$entity = 'Attachment';
|
||||||
|
foreach ($ops as $op) {
|
||||||
|
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||||
|
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||||
|
'display_name' => $op . ' ' . $entity . 's',
|
||||||
|
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||||
|
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||||
|
]);
|
||||||
|
DB::table('permission_role')->insert([
|
||||||
|
'role_id' => $adminRoleId,
|
||||||
|
'permission_id' => $permissionId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('attachments');
|
||||||
|
|
||||||
|
// Create & attach new entity permissions
|
||||||
|
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||||
|
$entity = 'Attachment';
|
||||||
|
foreach ($ops as $op) {
|
||||||
|
$permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
|
||||||
|
DB::table('role_permissions')->where('name', '=', $permName)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
gulpfile.js
29
gulpfile.js
|
@ -1,27 +1,8 @@
|
||||||
var elixir = require('laravel-elixir');
|
var elixir = require('laravel-elixir');
|
||||||
|
|
||||||
// Custom extensions
|
elixir(mix => {
|
||||||
var gulp = require('gulp');
|
mix.sass('styles.scss');
|
||||||
var Task = elixir.Task;
|
mix.sass('print-styles.scss');
|
||||||
var fs = require('fs');
|
mix.sass('export-styles.scss');
|
||||||
|
mix.browserify('global.js', './public/js/common.js');
|
||||||
elixir.extend('queryVersion', function(inputFiles) {
|
|
||||||
new Task('queryVersion', function() {
|
|
||||||
var manifestObject = {};
|
|
||||||
var uidString = Date.now().toString(16).slice(4);
|
|
||||||
for (var i = 0; i < inputFiles.length; i++) {
|
|
||||||
var file = inputFiles[i];
|
|
||||||
manifestObject[file] = file + '?version=' + uidString;
|
|
||||||
}
|
|
||||||
var fileContents = JSON.stringify(manifestObject, null, 1);
|
|
||||||
fs.writeFileSync('public/build/manifest.json', fileContents);
|
|
||||||
}).watch(['./public/css/*.css', './public/js/*.js']);
|
|
||||||
});
|
|
||||||
|
|
||||||
elixir(function(mix) {
|
|
||||||
mix.sass('styles.scss')
|
|
||||||
.sass('print-styles.scss')
|
|
||||||
.sass('export-styles.scss')
|
|
||||||
.browserify('global.js', 'public/js/common.js')
|
|
||||||
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
|
|
||||||
});
|
});
|
||||||
|
|
15
package.json
15
package.json
|
@ -1,18 +1,19 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"scripts": {
|
||||||
"gulp": "^3.9.0"
|
"prod": "gulp --production",
|
||||||
|
"dev": "gulp watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"angular": "^1.5.5",
|
"angular": "^1.5.5",
|
||||||
"angular-animate": "^1.5.5",
|
"angular-animate": "^1.5.5",
|
||||||
"angular-resource": "^1.5.5",
|
"angular-resource": "^1.5.5",
|
||||||
"angular-sanitize": "^1.5.5",
|
"angular-sanitize": "^1.5.5",
|
||||||
"angular-ui-sortable": "^0.14.0",
|
"angular-ui-sortable": "^0.15.0",
|
||||||
"babel-runtime": "^5.8.29",
|
|
||||||
"bootstrap-sass": "^3.0.0",
|
|
||||||
"dropzone": "^4.0.1",
|
"dropzone": "^4.0.1",
|
||||||
"laravel-elixir": "^5.0.0",
|
"gulp": "^3.9.0",
|
||||||
|
"laravel-elixir": "^6.0.0-11",
|
||||||
|
"laravel-elixir-browserify-official": "^0.1.3",
|
||||||
"marked": "^0.3.5",
|
"marked": "^0.3.5",
|
||||||
"moment": "^2.12.0",
|
"moment": "^2.12.0",
|
||||||
"zeroclipboard": "^2.2.0"
|
"zeroclipboard": "^2.2.0"
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
suites:
|
|
||||||
main:
|
|
||||||
namespace: BookStack
|
|
||||||
psr4_prefix: BookStack
|
|
||||||
src_path: app
|
|
|
@ -30,6 +30,7 @@
|
||||||
<env name="AUTH_METHOD" value="standard"/>
|
<env name="AUTH_METHOD" value="standard"/>
|
||||||
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||||
<env name="LDAP_VERSION" value="3"/>
|
<env name="LDAP_VERSION" value="3"/>
|
||||||
|
<env name="STORAGE_TYPE" value="local"/>
|
||||||
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||||
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||||
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
|
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||||
|
|
10
readme.md
10
readme.md
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
[](https://github.com/ssddanbrown/BookStack/releases/latest)
|
[](https://github.com/ssddanbrown/BookStack/releases/latest)
|
||||||
[](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
|
[](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
|
||||||
[](https://travis-ci.org/ssddanbrown/BookStack)
|
[](https://travis-ci.org/BookStackApp/BookStack)
|
||||||
|
|
||||||
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
|
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
|
||||||
|
|
||||||
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
||||||
* [Documentation](https://www.bookstackapp.com/docs)
|
* [Documentation](https://www.bookstackapp.com/docs)
|
||||||
* [Demo Instance](https://demo.bookstackapp.com) *(Login username: `admin@example.com`. Password: `password`)*
|
* [Demo Instance](https://demo.bookstackapp.com)
|
||||||
|
* *Username: `admin@example.com`*
|
||||||
|
* *Password: `password`*
|
||||||
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
||||||
|
|
||||||
## Development & Testing
|
## Development & Testing
|
||||||
|
@ -29,7 +31,7 @@ php artisan migrate --database=mysql_testing
|
||||||
php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
|
php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
|
||||||
```
|
```
|
||||||
|
|
||||||
Once done you can run `phpunit` (or `./vendor/bin/phpunit` if `phpunit` is not found) in the application root directory to run all tests.
|
Once done you can run `phpunit` in the application root directory to run all tests.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -51,3 +53,5 @@ These are the great projects used to help build BookStack:
|
||||||
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
||||||
* [Marked](https://github.com/chjj/marked)
|
* [Marked](https://github.com/chjj/marked)
|
||||||
* [Moment.js](http://momentjs.com/)
|
* [Moment.js](http://momentjs.com/)
|
||||||
|
|
||||||
|
Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy.
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="dropzone-container">
|
|
||||||
<div class="dz-message">Drop files or click here to upload</div>
|
|
||||||
</div>
|
|
|
@ -1,15 +0,0 @@
|
||||||
|
|
||||||
<div class="image-picker">
|
|
||||||
<div>
|
|
||||||
<img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
|
|
||||||
<img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
|
|
||||||
</div>
|
|
||||||
<button class="button" type="button" ng-click="showImageManager()">Select Image</button>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<button class="text-button" ng-click="reset()" type="button">Reset</button>
|
|
||||||
<span ng-show="showRemove" class="sep">|</span>
|
|
||||||
<button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
|
|
||||||
|
|
||||||
<input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
|
|
||||||
</div>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
|
|
||||||
<input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
|
|
||||||
<div class="switch-handle"></div>
|
|
||||||
</div>
|
|
|
@ -1,8 +1,10 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const moment = require('moment');
|
import moment from 'moment';
|
||||||
|
import 'moment/locale/en-gb';
|
||||||
|
moment.locale('en-gb');
|
||||||
|
|
||||||
module.exports = function (ngApp, events) {
|
export default function (ngApp, events) {
|
||||||
|
|
||||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||||
|
@ -17,7 +19,7 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.imageDeleteSuccess = false;
|
$scope.imageDeleteSuccess = false;
|
||||||
$scope.uploadedTo = $attrs.uploadedTo;
|
$scope.uploadedTo = $attrs.uploadedTo;
|
||||||
$scope.view = 'all';
|
$scope.view = 'all';
|
||||||
|
|
||||||
$scope.searching = false;
|
$scope.searching = false;
|
||||||
$scope.searchTerm = '';
|
$scope.searchTerm = '';
|
||||||
|
|
||||||
|
@ -48,7 +50,7 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.hasMore = preSearchHasMore;
|
$scope.hasMore = preSearchHasMore;
|
||||||
}
|
}
|
||||||
$scope.cancelSearch = cancelSearch;
|
$scope.cancelSearch = cancelSearch;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs on image upload, Adds an image to local list of images
|
* Runs on image upload, Adds an image to local list of images
|
||||||
|
@ -162,7 +164,6 @@ module.exports = function (ngApp, events) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a search operation
|
* Start a search operation
|
||||||
* @param searchTerm
|
|
||||||
*/
|
*/
|
||||||
$scope.searchImages = function() {
|
$scope.searchImages = function() {
|
||||||
|
|
||||||
|
@ -196,7 +197,7 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.view = viewName;
|
$scope.view = viewName;
|
||||||
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
|
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the details of an image.
|
* Save the details of an image.
|
||||||
|
@ -205,7 +206,7 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.saveImageDetails = function (event) {
|
$scope.saveImageDetails = function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
|
var url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
|
||||||
$http.put(url, this.selectedImage).then((response) => {
|
$http.put(url, this.selectedImage).then(response => {
|
||||||
events.emit('success', 'Image details updated');
|
events.emit('success', 'Image details updated');
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
if (response.status === 422) {
|
if (response.status === 422) {
|
||||||
|
@ -300,15 +301,16 @@ module.exports = function (ngApp, events) {
|
||||||
var isEdit = pageId !== 0;
|
var isEdit = pageId !== 0;
|
||||||
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
||||||
var isMarkdown = $attrs.editorType === 'markdown';
|
var isMarkdown = $attrs.editorType === 'markdown';
|
||||||
|
$scope.draftsEnabled = $attrs.draftsEnabled === 'true';
|
||||||
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
||||||
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
||||||
|
|
||||||
// Set inital header draft text
|
// Set initial header draft text
|
||||||
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
||||||
$scope.draftText = 'Editing Draft'
|
$scope.draftText = 'Editing Draft'
|
||||||
} else {
|
} else {
|
||||||
$scope.draftText = 'Editing Page'
|
$scope.draftText = 'Editing Page'
|
||||||
};
|
}
|
||||||
|
|
||||||
var autoSave = false;
|
var autoSave = false;
|
||||||
|
|
||||||
|
@ -317,7 +319,7 @@ module.exports = function (ngApp, events) {
|
||||||
html: false
|
html: false
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit && $scope.draftsEnabled) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startAutoSave();
|
startAutoSave();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
@ -336,6 +338,8 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.editorChange = function() {};
|
$scope.editorChange = function() {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastSave = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the AutoSave loop, Checks for content change
|
* Start the AutoSave loop, Checks for content change
|
||||||
* before performing the costly AJAX request.
|
* before performing the costly AJAX request.
|
||||||
|
@ -345,6 +349,8 @@ module.exports = function (ngApp, events) {
|
||||||
currentContent.html = $scope.editContent;
|
currentContent.html = $scope.editContent;
|
||||||
|
|
||||||
autoSave = $interval(() => {
|
autoSave = $interval(() => {
|
||||||
|
// Return if manually saved recently to prevent bombarding the server
|
||||||
|
if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return;
|
||||||
var newTitle = $('#name').val();
|
var newTitle = $('#name').val();
|
||||||
var newHtml = $scope.editContent;
|
var newHtml = $scope.editContent;
|
||||||
|
|
||||||
|
@ -357,10 +363,12 @@ module.exports = function (ngApp, events) {
|
||||||
}, 1000 * autosaveFrequency);
|
}, 1000 * autosaveFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let draftErroring = false;
|
||||||
/**
|
/**
|
||||||
* Save a draft update into the system via an AJAX request.
|
* Save a draft update into the system via an AJAX request.
|
||||||
*/
|
*/
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
|
if (!$scope.draftsEnabled) return;
|
||||||
var data = {
|
var data = {
|
||||||
name: $('#name').val(),
|
name: $('#name').val(),
|
||||||
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
|
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
|
||||||
|
@ -369,11 +377,17 @@ module.exports = function (ngApp, events) {
|
||||||
if (isMarkdown) data.markdown = $scope.editContent;
|
if (isMarkdown) data.markdown = $scope.editContent;
|
||||||
|
|
||||||
let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
|
let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
|
||||||
$http.put(url, data).then((responseData) => {
|
$http.put(url, data).then(responseData => {
|
||||||
|
draftErroring = false;
|
||||||
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
|
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
|
||||||
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
|
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
|
||||||
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
||||||
showDraftSaveNotification();
|
showDraftSaveNotification();
|
||||||
|
lastSave = Date.now();
|
||||||
|
}, errorRes => {
|
||||||
|
if (draftErroring) return;
|
||||||
|
events.emit('error', 'Failed to save draft. Ensure you have internet connection before saving this page.')
|
||||||
|
draftErroring = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -424,7 +438,7 @@ module.exports = function (ngApp, events) {
|
||||||
|
|
||||||
const pageId = Number($attrs.pageId);
|
const pageId = Number($attrs.pageId);
|
||||||
$scope.tags = [];
|
$scope.tags = [];
|
||||||
|
|
||||||
$scope.sortOptions = {
|
$scope.sortOptions = {
|
||||||
handle: '.handle',
|
handle: '.handle',
|
||||||
items: '> tr',
|
items: '> tr',
|
||||||
|
@ -447,7 +461,7 @@ module.exports = function (ngApp, events) {
|
||||||
* Get all tags for the current book and add into scope.
|
* Get all tags for the current book and add into scope.
|
||||||
*/
|
*/
|
||||||
function getTags() {
|
function getTags() {
|
||||||
let url = window.baseUrl('/ajax/tags/get/page/' + pageId);
|
let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
|
||||||
$http.get(url).then((responseData) => {
|
$http.get(url).then((responseData) => {
|
||||||
$scope.tags = responseData.data;
|
$scope.tags = responseData.data;
|
||||||
addEmptyTag();
|
addEmptyTag();
|
||||||
|
@ -516,21 +530,201 @@ module.exports = function (ngApp, events) {
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
||||||
|
ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
|
||||||
|
function ($scope, $http, $attrs) {
|
||||||
|
|
||||||
|
const pageId = $scope.uploadedTo = $attrs.pageId;
|
||||||
|
let currentOrder = '';
|
||||||
|
$scope.files = [];
|
||||||
|
$scope.editFile = false;
|
||||||
|
$scope.file = getCleanFile();
|
||||||
|
$scope.errors = {
|
||||||
|
link: {},
|
||||||
|
edit: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCleanFile() {
|
||||||
|
return {
|
||||||
|
page_id: pageId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Angular-UI-Sort options
|
||||||
|
$scope.sortOptions = {
|
||||||
|
handle: '.handle',
|
||||||
|
items: '> tr',
|
||||||
|
containment: "parent",
|
||||||
|
axis: "y",
|
||||||
|
stop: sortUpdate,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for sort changes.
|
||||||
|
* Updates the file ordering on the server.
|
||||||
|
* @param event
|
||||||
|
* @param ui
|
||||||
|
*/
|
||||||
|
function sortUpdate(event, ui) {
|
||||||
|
let newOrder = $scope.files.map(file => {return file.id}).join(':');
|
||||||
|
if (newOrder === currentOrder) return;
|
||||||
|
|
||||||
|
currentOrder = newOrder;
|
||||||
|
$http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
|
||||||
|
events.emit('success', resp.data.message);
|
||||||
|
}, checkError('sort'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by dropzone to get the endpoint to upload to.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
$scope.getUploadUrl = function (file) {
|
||||||
|
let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
|
||||||
|
return window.baseUrl(`/attachments/upload${suffix}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get files for the current page from the server.
|
||||||
|
*/
|
||||||
|
function getFiles() {
|
||||||
|
let url = window.baseUrl(`/attachments/get/page/${pageId}`)
|
||||||
|
$http.get(url).then(resp => {
|
||||||
|
$scope.files = resp.data;
|
||||||
|
currentOrder = resp.data.map(file => {return file.id}).join(':');
|
||||||
|
}, checkError('get'));
|
||||||
|
}
|
||||||
|
getFiles();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs on file upload, Adds an file to local file list
|
||||||
|
* and shows a success message to the user.
|
||||||
|
* @param file
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
$scope.uploadSuccess = function (file, data) {
|
||||||
|
$scope.$apply(() => {
|
||||||
|
$scope.files.push(data);
|
||||||
|
});
|
||||||
|
events.emit('success', 'File uploaded');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload and overwrite an existing file.
|
||||||
|
* @param file
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
$scope.uploadSuccessUpdate = function (file, data) {
|
||||||
|
$scope.$apply(() => {
|
||||||
|
let search = filesIndexOf(data);
|
||||||
|
if (search !== -1) $scope.files[search] = data;
|
||||||
|
|
||||||
|
if ($scope.editFile) {
|
||||||
|
$scope.editFile = angular.copy(data);
|
||||||
|
data.link = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
events.emit('success', 'File updated');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file from the server and, on success, the local listing.
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
$scope.deleteFile = function(file) {
|
||||||
|
if (!file.deleting) {
|
||||||
|
file.deleting = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
|
||||||
|
events.emit('success', resp.data.message);
|
||||||
|
$scope.files.splice($scope.files.indexOf(file), 1);
|
||||||
|
}, checkError('delete'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a link to a page.
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
$scope.attachLinkSubmit = function(file) {
|
||||||
|
file.uploaded_to = pageId;
|
||||||
|
$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
|
||||||
|
$scope.files.push(resp.data);
|
||||||
|
events.emit('success', 'Link attached');
|
||||||
|
$scope.file = getCleanFile();
|
||||||
|
}, checkError('link'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the edit mode for a file.
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
$scope.startEdit = function(file) {
|
||||||
|
$scope.editFile = angular.copy(file);
|
||||||
|
$scope.editFile.link = (file.external) ? file.path : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel edit mode
|
||||||
|
*/
|
||||||
|
$scope.cancelEdit = function() {
|
||||||
|
$scope.editFile = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the name and link of a file.
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
$scope.updateFile = function(file) {
|
||||||
|
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
|
||||||
|
let search = filesIndexOf(resp.data);
|
||||||
|
if (search !== -1) $scope.files[search] = resp.data;
|
||||||
|
|
||||||
|
if ($scope.editFile && !file.external) {
|
||||||
|
$scope.editFile.link = '';
|
||||||
|
}
|
||||||
|
$scope.editFile = false;
|
||||||
|
events.emit('success', 'Attachment details updated');
|
||||||
|
}, checkError('edit'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the url of a file.
|
||||||
|
*/
|
||||||
|
$scope.getFileUrl = function(file) {
|
||||||
|
return window.baseUrl('/attachments/' + file.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the local files via another file object.
|
||||||
|
* Used to search via object copies.
|
||||||
|
* @param file
|
||||||
|
* @returns int
|
||||||
|
*/
|
||||||
|
function filesIndexOf(file) {
|
||||||
|
for (let i = 0; i < $scope.files.length; i++) {
|
||||||
|
if ($scope.files[i].id == file.id) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for an error response in a ajax request.
|
||||||
|
* @param errorGroupName
|
||||||
|
*/
|
||||||
|
function checkError(errorGroupName) {
|
||||||
|
$scope.errors[errorGroupName] = {};
|
||||||
|
return function(response) {
|
||||||
|
if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
|
||||||
|
events.emit('error', response.data.error);
|
||||||
|
}
|
||||||
|
if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
|
||||||
|
$scope.errors[errorGroupName] = response.data.validation;
|
||||||
|
console.log($scope.errors[errorGroupName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}]);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
const DropZone = require('dropzone');
|
const DropZone = require('dropzone');
|
||||||
const markdown = require('marked');
|
const markdown = require('marked');
|
||||||
|
|
||||||
const toggleSwitchTemplate = require('./components/toggle-switch.html');
|
|
||||||
const imagePickerTemplate = require('./components/image-picker.html');
|
|
||||||
const dropZoneTemplate = require('./components/drop-zone.html');
|
|
||||||
|
|
||||||
module.exports = function (ngApp, events) {
|
module.exports = function (ngApp, events) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +12,12 @@ module.exports = function (ngApp, events) {
|
||||||
ngApp.directive('toggleSwitch', function () {
|
ngApp.directive('toggleSwitch', function () {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
template: toggleSwitchTemplate,
|
template: `
|
||||||
|
<div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
|
||||||
|
<input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
|
||||||
|
<div class="switch-handle"></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
scope: true,
|
scope: true,
|
||||||
link: function (scope, element, attrs) {
|
link: function (scope, element, attrs) {
|
||||||
scope.name = attrs.name;
|
scope.name = attrs.name;
|
||||||
|
@ -33,6 +34,59 @@ module.exports = function (ngApp, events) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common tab controls using simple jQuery functions.
|
||||||
|
*/
|
||||||
|
ngApp.directive('tabContainer', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
const $content = element.find('[tab-content]');
|
||||||
|
const $buttons = element.find('[tab-button]');
|
||||||
|
|
||||||
|
if (attrs.tabContainer) {
|
||||||
|
let initial = attrs.tabContainer;
|
||||||
|
$buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
|
||||||
|
$content.hide().filter(`[tab-content="${initial}"]`).show();
|
||||||
|
} else {
|
||||||
|
$content.hide().first().show();
|
||||||
|
$buttons.first().addClass('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
$buttons.click(function() {
|
||||||
|
let clickedTab = $(this);
|
||||||
|
$buttons.removeClass('selected');
|
||||||
|
$content.hide();
|
||||||
|
let name = clickedTab.addClass('selected').attr('tab-button');
|
||||||
|
$content.filter(`[tab-content="${name}"]`).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub form component to allow inner-form sections to act like thier own forms.
|
||||||
|
*/
|
||||||
|
ngApp.directive('subForm', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
element.on('keypress', e => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
submitEvent(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
element.find('button[type="submit"]').click(submitEvent);
|
||||||
|
|
||||||
|
function submitEvent(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (attrs.subForm) scope.$eval(attrs.subForm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Picker
|
* Image Picker
|
||||||
|
@ -41,7 +95,22 @@ module.exports = function (ngApp, events) {
|
||||||
ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
|
ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: imagePickerTemplate,
|
template: `
|
||||||
|
<div class="image-picker">
|
||||||
|
<div>
|
||||||
|
<img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
|
||||||
|
<img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
|
||||||
|
</div>
|
||||||
|
<button class="button" type="button" ng-click="showImageManager()">Select Image</button>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<button class="text-button" ng-click="reset()" type="button">Reset</button>
|
||||||
|
<span ng-show="showRemove" class="sep">|</span>
|
||||||
|
<button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
|
||||||
|
|
||||||
|
<input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
scope: {
|
scope: {
|
||||||
name: '@',
|
name: '@',
|
||||||
resizeHeight: '@',
|
resizeHeight: '@',
|
||||||
|
@ -108,7 +177,11 @@ module.exports = function (ngApp, events) {
|
||||||
ngApp.directive('dropZone', [function () {
|
ngApp.directive('dropZone', [function () {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: dropZoneTemplate,
|
template: `
|
||||||
|
<div class="dropzone-container">
|
||||||
|
<div class="dz-message">Drop files or click here to upload</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
scope: {
|
scope: {
|
||||||
uploadUrl: '@',
|
uploadUrl: '@',
|
||||||
eventSuccess: '=',
|
eventSuccess: '=',
|
||||||
|
@ -116,6 +189,7 @@ module.exports = function (ngApp, events) {
|
||||||
uploadedTo: '@'
|
uploadedTo: '@'
|
||||||
},
|
},
|
||||||
link: function (scope, element, attrs) {
|
link: function (scope, element, attrs) {
|
||||||
|
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
|
||||||
var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
|
var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
|
||||||
url: scope.uploadUrl,
|
url: scope.uploadUrl,
|
||||||
init: function () {
|
init: function () {
|
||||||
|
@ -488,8 +562,8 @@ module.exports = function (ngApp, events) {
|
||||||
link: function (scope, elem, attrs) {
|
link: function (scope, elem, attrs) {
|
||||||
|
|
||||||
// Get common elements
|
// Get common elements
|
||||||
const $buttons = elem.find('[tab-button]');
|
const $buttons = elem.find('[toolbox-tab-button]');
|
||||||
const $content = elem.find('[tab-content]');
|
const $content = elem.find('[toolbox-tab-content]');
|
||||||
const $toggle = elem.find('[toolbox-toggle]');
|
const $toggle = elem.find('[toolbox-toggle]');
|
||||||
|
|
||||||
// Handle toolbox toggle click
|
// Handle toolbox toggle click
|
||||||
|
@ -501,17 +575,17 @@ module.exports = function (ngApp, events) {
|
||||||
function setActive(tabName, openToolbox) {
|
function setActive(tabName, openToolbox) {
|
||||||
$buttons.removeClass('active');
|
$buttons.removeClass('active');
|
||||||
$content.hide();
|
$content.hide();
|
||||||
$buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
|
$buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
|
||||||
$content.filter(`[tab-content="${tabName}"]`).show();
|
$content.filter(`[toolbox-tab-content="${tabName}"]`).show();
|
||||||
if (openToolbox) elem.addClass('open');
|
if (openToolbox) elem.addClass('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the first tab content active on load
|
// Set the first tab content active on load
|
||||||
setActive($content.first().attr('tab-content'), false);
|
setActive($content.first().attr('toolbox-tab-content'), false);
|
||||||
|
|
||||||
// Handle tab button click
|
// Handle tab button click
|
||||||
$buttons.click(function (e) {
|
$buttons.click(function (e) {
|
||||||
let name = $(this).attr('tab-button');
|
let name = $(this).attr('toolbox-tab-button');
|
||||||
setActive(name, true);
|
setActive(name, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -549,7 +623,7 @@ module.exports = function (ngApp, events) {
|
||||||
let val = $input.val();
|
let val = $input.val();
|
||||||
let url = $input.attr('autosuggest');
|
let url = $input.attr('autosuggest');
|
||||||
let type = $input.attr('autosuggest-type');
|
let type = $input.attr('autosuggest-type');
|
||||||
|
|
||||||
// Add name param to request if for a value
|
// Add name param to request if for a value
|
||||||
if (type.toLowerCase() === 'value') {
|
if (type.toLowerCase() === 'value') {
|
||||||
let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
|
let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
|
||||||
|
@ -850,17 +924,3 @@ module.exports = function (ngApp, events) {
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -38,13 +38,17 @@ class EventManager {
|
||||||
this.listeners[eventName].push(callback);
|
this.listeners[eventName].push(callback);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
window.Events = new EventManager();
|
window.Events = new EventManager();
|
||||||
|
|
||||||
|
// Load in angular specific items
|
||||||
var services = require('./services')(ngApp, window.Events);
|
import Services from './services';
|
||||||
var directives = require('./directives')(ngApp, window.Events);
|
import Directives from './directives';
|
||||||
var controllers = require('./controllers')(ngApp, window.Events);
|
import Controllers from './controllers';
|
||||||
|
Services(ngApp, window.Events);
|
||||||
|
Directives(ngApp, window.Events);
|
||||||
|
Controllers(ngApp, window.Events);
|
||||||
|
|
||||||
//Global jQuery Config & Extensions
|
//Global jQuery Config & Extensions
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
* @param editor - editor instance
|
* @param editor - editor instance
|
||||||
*/
|
*/
|
||||||
function editorPaste(e, editor) {
|
function editorPaste(e, editor) {
|
||||||
if (!e.clipboardData) return
|
if (!e.clipboardData) return;
|
||||||
let items = e.clipboardData.items;
|
let items = e.clipboardData.items;
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
if (items[i].type.indexOf("image") === -1) return
|
if (items[i].type.indexOf("image") === -1) return;
|
||||||
|
|
||||||
let file = items[i].getAsFile();
|
let file = items[i].getAsFile();
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
@ -81,9 +81,10 @@ var mceOptions = module.exports = {
|
||||||
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 | 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;}",
|
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
|
||||||
style_formats: [
|
style_formats: [
|
||||||
{title: "Header 1", format: "h1"},
|
{title: "Header Large", format: "h2"},
|
||||||
{title: "Header 2", format: "h2"},
|
{title: "Header Medium", format: "h3"},
|
||||||
{title: "Header 3", format: "h3"},
|
{title: "Header Small", format: "h4"},
|
||||||
|
{title: "Header Tiny", format: "h5"},
|
||||||
{title: "Paragraph", format: "p", exact: true, classes: ''},
|
{title: "Paragraph", format: "p", exact: true, classes: ''},
|
||||||
{title: "Blockquote", format: "blockquote"},
|
{title: "Blockquote", format: "blockquote"},
|
||||||
{title: "Code Block", icon: "code", format: "pre"},
|
{title: "Code Block", icon: "code", format: "pre"},
|
||||||
|
|
|
@ -43,10 +43,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//body.ie .popup-body {
|
|
||||||
// min-height: 100%;
|
|
||||||
//}
|
|
||||||
|
|
||||||
.corner-button {
|
.corner-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
min-height: 70vh;
|
min-height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-manager .dropzone-container {
|
.dropzone-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 3px dashed #DDD;
|
border: 3px dashed #DDD;
|
||||||
}
|
}
|
||||||
|
@ -456,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
border-right: 6px solid transparent;
|
border-right: 6px solid transparent;
|
||||||
border-bottom: 6px solid $negative;
|
border-bottom: 6px solid $negative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[tab-container] .nav-tabs {
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
margin-bottom: $-m;
|
||||||
|
.tab-item {
|
||||||
|
padding: $-s;
|
||||||
|
color: #666;
|
||||||
|
&.selected {
|
||||||
|
border-bottom-width: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
.page-list {
|
.page-list {
|
||||||
h3 {
|
h4 {
|
||||||
margin: $-l 0 $-xs 0;
|
margin: $-l 0 $-xs 0;
|
||||||
font-size: 1.666em;
|
font-size: 1.666em;
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,13 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: $-l;
|
margin-bottom: $-l;
|
||||||
}
|
}
|
||||||
h4 {
|
h5 {
|
||||||
display: block;
|
display: block;
|
||||||
margin: $-s 0 0 0;
|
margin: $-s 0 0 0;
|
||||||
border-left: 5px solid $color-page;
|
border-left: 5px solid $color-page;
|
||||||
padding: $-xs 0 $-xs $-m;
|
padding: $-xs 0 $-xs $-m;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: normal;
|
||||||
&.draft {
|
&.draft {
|
||||||
border-left-color: $color-page-draft;
|
border-left-color: $color-page-draft;
|
||||||
}
|
}
|
||||||
|
@ -67,44 +69,39 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-nav-list {
|
.sidebar-page-nav {
|
||||||
$nav-indent: $-s;
|
$nav-indent: $-s;
|
||||||
margin-left: 2px;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
margin: $-s 0 $-m 2px;
|
||||||
|
border-left: 2px dotted #BBB;
|
||||||
li {
|
li {
|
||||||
//border-left: 1px solid rgba(0, 0, 0, 0.1);
|
padding-left: $-s;
|
||||||
padding-left: $-xs;
|
|
||||||
border-left: 2px solid #888;
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
|
||||||
li a {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.nav-H2 {
|
|
||||||
margin-left: $nav-indent;
|
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
.nav-H3 {
|
.h1 {
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
.h2 {
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
.h3 {
|
||||||
|
margin-left: $nav-indent;
|
||||||
|
}
|
||||||
|
.h4 {
|
||||||
margin-left: $nav-indent*2;
|
margin-left: $nav-indent*2;
|
||||||
font-size: 0.90em
|
|
||||||
}
|
}
|
||||||
.nav-H4 {
|
.h5 {
|
||||||
margin-left: $nav-indent*3;
|
margin-left: $nav-indent*3;
|
||||||
font-size: 0.85em
|
|
||||||
}
|
}
|
||||||
.nav-H5 {
|
.h6 {
|
||||||
margin-left: $nav-indent*4;
|
margin-left: $nav-indent*4;
|
||||||
font-size: 0.80em
|
|
||||||
}
|
|
||||||
.nav-H6 {
|
|
||||||
margin-left: $nav-indent*5;
|
|
||||||
font-size: 0.75em
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar list
|
// Sidebar list
|
||||||
.book-tree {
|
.book-tree {
|
||||||
padding: $-l 0 0 0;
|
padding: $-xs 0 0 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -306,10 +303,10 @@ ul.pagination {
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-list {
|
.entity-list {
|
||||||
>div {
|
> div {
|
||||||
padding: $-m 0;
|
padding: $-m 0;
|
||||||
}
|
}
|
||||||
h3 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
|
@ -327,9 +324,10 @@ ul.pagination {
|
||||||
color: $color-page-draft;
|
color: $color-page-draft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-list.compact {
|
.entity-list.compact {
|
||||||
font-size: 0.6em;
|
font-size: 0.6em;
|
||||||
h3, a {
|
h4, a {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
|
|
|
@ -71,6 +71,18 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// diffs
|
||||||
|
ins,
|
||||||
|
del {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
ins {
|
||||||
|
background: #dbffdb;
|
||||||
|
}
|
||||||
|
del {
|
||||||
|
background: #FFECEC;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page content pointers
|
// Page content pointers
|
||||||
|
@ -138,7 +150,6 @@
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
border: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
right: $-xl*2;
|
right: $-xl*2;
|
||||||
z-index: 99;
|
|
||||||
width: 48px;
|
width: 48px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
@ -189,7 +200,7 @@
|
||||||
color: #444;
|
color: #444;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
div[tab-content] {
|
div[toolbox-tab-content] {
|
||||||
padding-bottom: 45px;
|
padding-bottom: 45px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -197,7 +208,7 @@
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
div[tab-content] .padded {
|
div[toolbox-tab-content] .padded {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
@ -216,21 +227,6 @@
|
||||||
padding-top: $-s;
|
padding-top: $-s;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
button.pos {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: $-s;
|
|
||||||
height: 45px;
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
border-radius: 0;
|
|
||||||
&:hover{
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.handle {
|
.handle {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
@ -242,9 +238,12 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
table td, table th {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[tab-content] {
|
[toolbox-tab-content] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,4 +51,14 @@ table.list-table {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: $-xs;
|
padding: $-xs;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.file-table {
|
||||||
|
@extend .no-style;
|
||||||
|
td {
|
||||||
|
padding: $-xs;
|
||||||
|
}
|
||||||
|
.ui-sortable-helper {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -15,31 +15,41 @@ h2 {
|
||||||
margin-bottom: 0.43137255em;
|
margin-bottom: 0.43137255em;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.75em;
|
font-size: 2.333em;
|
||||||
line-height: 1.571428572em;
|
line-height: 1.571428572em;
|
||||||
margin-top: 0.78571429em;
|
margin-top: 0.78571429em;
|
||||||
margin-bottom: 0.43137255em;
|
margin-bottom: 0.43137255em;
|
||||||
}
|
}
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1em;
|
font-size: 1.666em;
|
||||||
line-height: 1.375em;
|
line-height: 1.375em;
|
||||||
margin-top: 0.78571429em;
|
margin-top: 0.78571429em;
|
||||||
margin-bottom: 0.43137255em;
|
margin-bottom: 0.43137255em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
color: #555;
|
color: #555;
|
||||||
.subheader {
|
.subheader {
|
||||||
//display: block;
|
|
||||||
font-size: 0.5em;
|
font-size: 0.5em;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
color: lighten($text-dark, 32%);
|
color: lighten($text-dark, 32%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5, h6 {
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2em;
|
||||||
|
margin-top: 0.78571429em;
|
||||||
|
margin-bottom: 0.66em;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Link styling
|
* Link styling
|
||||||
*/
|
*/
|
||||||
|
@ -183,7 +193,7 @@ p.neg, p .neg, span.neg, .text-neg {
|
||||||
p.muted, p .muted, span.muted, .text-muted {
|
p.muted, p .muted, span.muted, .text-muted {
|
||||||
color: lighten($text-dark, 26%);
|
color: lighten($text-dark, 26%);
|
||||||
&.small, .small {
|
&.small, .small {
|
||||||
color: lighten($text-dark, 42%);
|
color: lighten($text-dark, 32%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity text strings.
|
||||||
|
* Is used for all the text within activity logs & notifications.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
'page_create' => 'Seite erstellt',
|
||||||
|
'page_create_notification' => 'Seite erfolgreich erstellt',
|
||||||
|
'page_update' => 'Seite aktualisiert',
|
||||||
|
'page_update_notification' => 'Seite erfolgreich aktualisiert',
|
||||||
|
'page_delete' => 'Seite gelöscht',
|
||||||
|
'page_delete_notification' => 'Seite erfolgreich gelöscht',
|
||||||
|
'page_restore' => 'Seite wiederhergstellt',
|
||||||
|
'page_restore_notification' => 'Seite erfolgreich wiederhergstellt',
|
||||||
|
'page_move' => 'Seite verschoben',
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
'chapter_create' => 'Kapitel erstellt',
|
||||||
|
'chapter_create_notification' => 'Kapitel erfolgreich erstellt',
|
||||||
|
'chapter_update' => 'Kapitel aktualisiert',
|
||||||
|
'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',
|
||||||
|
'chapter_delete' => 'Kapitel gelöscht',
|
||||||
|
'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht',
|
||||||
|
'chapter_move' => 'Kapitel verschoben',
|
||||||
|
|
||||||
|
// Books
|
||||||
|
'book_create' => 'Buch erstellt',
|
||||||
|
'book_create_notification' => 'Buch erfolgreich erstellt',
|
||||||
|
'book_update' => 'Buch aktualisiert',
|
||||||
|
'book_update_notification' => 'Buch erfolgreich aktualisiert',
|
||||||
|
'book_delete' => 'Buch gelöscht',
|
||||||
|
'book_delete_notification' => 'Buch erfolgreich gelöscht',
|
||||||
|
'book_sort' => 'Buch sortiert',
|
||||||
|
'book_sort_notification' => 'Buch erfolgreich neu sortiert',
|
||||||
|
|
||||||
|
];
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Language Lines
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following language lines are used during authentication for various
|
||||||
|
| messages that we need to display to the user. You are free to modify
|
||||||
|
| these language lines according to your application's requirements.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'failed' => 'Dies sind keine gültigen Anmeldedaten.',
|
||||||
|
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email Confirmation Text
|
||||||
|
*/
|
||||||
|
'email_confirm_subject' => 'Bestätigen sie ihre E-Mail Adresse bei :appName',
|
||||||
|
'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!',
|
||||||
|
'email_confirm_text' => 'Bitte bestätigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:',
|
||||||
|
'email_confirm_action' => 'E-Mail Adresse bestätigen',
|
||||||
|
'email_confirm_send_error' => 'Bestätigungs-E-Mail benötigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.',
|
||||||
|
'email_confirm_success' => 'Ihre E-Mail Adresse wurde bestätigt!',
|
||||||
|
'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen sie ihren Posteingang.',
|
||||||
|
];
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error text strings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
|
||||||
|
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuführen.'
|
||||||
|
];
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Pagination Language Lines
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following language lines are used by the paginator library to build
|
||||||
|
| the simple pagination links. You are free to change them to anything
|
||||||
|
| you want to customize your views to better match your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'previous' => '« Vorherige',
|
||||||
|
'next' => 'Nächste »',
|
||||||
|
|
||||||
|
];
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Reminder Language Lines
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following language lines are the default lines which match reasons
|
||||||
|
| that are given by the password broker for a password update attempt
|
||||||
|
| has failed, such as for an invalid token or invalid new password.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password' => 'Passörter müssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.',
|
||||||
|
'user' => "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.",
|
||||||
|
'token' => 'Dieser Passwort-Reset-Token ist ungültig.',
|
||||||
|
'sent' => 'Wir haben ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!',
|
||||||
|
'reset' => 'Ihr Passwort wurde zurückgesetzt!',
|
||||||
|
|
||||||
|
];
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings text strings
|
||||||
|
* Contains all text strings used in the general settings sections of BookStack
|
||||||
|
* including users and roles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'settings' => 'Einstellungen',
|
||||||
|
'settings_save' => 'Einstellungen speichern',
|
||||||
|
|
||||||
|
'app_settings' => 'Anwendungseinstellungen',
|
||||||
|
'app_name' => 'Anwendungsname',
|
||||||
|
'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
|
||||||
|
'app_name_header' => 'Anwendungsname im Header anzeigen?',
|
||||||
|
'app_public_viewing' => 'Öffentliche Ansicht erlauben?',
|
||||||
|
'app_secure_images' => 'Erh&oml;hte Sicherheit für Bilduploads aktivieren?',
|
||||||
|
'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
|
||||||
|
'app_editor' => 'Seiteneditor',
|
||||||
|
'app_editor_desc' => 'Wählen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
|
||||||
|
'app_custom_html' => 'Benutzerdefinierter HTML <head> Inhalt',
|
||||||
|
'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der <head> Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics Code hinzuzufügen.',
|
||||||
|
'app_logo' => 'Anwendungslogo',
|
||||||
|
'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein. <br>Größere Bilder werden verkleinert.',
|
||||||
|
'app_primary_color' => 'Primäre Anwendungsfarbe',
|
||||||
|
'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>Leer lassen des Feldes setzt auf die Standard-Anwendungsfarbe zurück.',
|
||||||
|
|
||||||
|
'reg_settings' => 'Registrierungseinstellungen',
|
||||||
|
'reg_allow' => 'Registrierung erlauben?',
|
||||||
|
'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
|
||||||
|
'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?',
|
||||||
|
'reg_confirm_email_desc' => 'Falls die Einschränkung für; Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
|
||||||
|
'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',
|
||||||
|
'reg_confirm_restrict_domain_desc' => 'Fügen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können. <br> Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.',
|
||||||
|
'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',
|
||||||
|
|
||||||
|
];
|
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Validation Language Lines
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| following language lines contain default error messages used by
|
||||||
|
| validator class. Some of these rules have multiple versions such
|
||||||
|
| as size rules. Feel free to tweak each of these messages here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'accepted' => ':attribute muss akzeptiert werden.',
|
||||||
|
'active_url' => ':attribute ist keine valide URL.',
|
||||||
|
'after' => ':attribute muss ein Datum nach :date sein.',
|
||||||
|
'alpha' => ':attribute kann nur Buchstaben enthalten.',
|
||||||
|
'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',
|
||||||
|
'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.',
|
||||||
|
'array' => ':attribute muss eine Array sein.',
|
||||||
|
'before' => ':attribute muss ein Datum vor :date sein.',
|
||||||
|
'between' => [
|
||||||
|
'numeric' => ':attribute muss zwischen :min und :max liegen.',
|
||||||
|
'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.',
|
||||||
|
'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.',
|
||||||
|
'array' => ':attribute muss zwischen :min und :max Elemente enthalten.',
|
||||||
|
],
|
||||||
|
'boolean' => ':attribute Feld muss wahr oder falsch sein.',
|
||||||
|
'confirmed' => ':attribute Bestätigung stimmt nicht überein.',
|
||||||
|
'date' => ':attribute ist kein valides Datum.',
|
||||||
|
'date_format' => ':attribute entspricht nicht dem Format :format.',
|
||||||
|
'different' => ':attribute und :other müssen unterschiedlich sein.',
|
||||||
|
'digits' => ':attribute muss :digits Stellen haben.',
|
||||||
|
'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.',
|
||||||
|
'email' => ':attribute muss eine valide E-Mail Adresse sein.',
|
||||||
|
'filled' => ':attribute Feld ist erforderlich.',
|
||||||
|
'exists' => 'Markiertes :attribute ist ungültig.',
|
||||||
|
'image' => ':attribute muss ein Bild sein.',
|
||||||
|
'in' => 'Markiertes :attribute ist ungültig.',
|
||||||
|
'integer' => ':attribute muss eine Zahl sein.',
|
||||||
|
'ip' => ':attribute muss eine valide IP-Adresse sein.',
|
||||||
|
'max' => [
|
||||||
|
'numeric' => ':attribute darf nicht größer als :max sein.',
|
||||||
|
'file' => ':attribute darf nicht größer als :max Kilobyte sein.',
|
||||||
|
'string' => ':attribute darf nicht länger als :max Zeichen sein.',
|
||||||
|
'array' => ':attribute darf nicht mehr als :max Elemente enthalten.',
|
||||||
|
],
|
||||||
|
'mimes' => ':attribute muss eine Datei vom Typ: :values sein.',
|
||||||
|
'min' => [
|
||||||
|
'numeric' => ':attribute muss mindestens :min. sein',
|
||||||
|
'file' => ':attribute muss mindestens :min Kilobyte groß sein.',
|
||||||
|
'string' => ':attribute muss mindestens :min Zeichen lang sein.',
|
||||||
|
'array' => ':attribute muss mindesten :min Elemente enthalten.',
|
||||||
|
],
|
||||||
|
'not_in' => 'Markiertes :attribute ist ungültig.',
|
||||||
|
'numeric' => ':attribute muss eine Zahl sein.',
|
||||||
|
'regex' => ':attribute Format ist ungültig.',
|
||||||
|
'required' => ':attribute Feld ist erforderlich.',
|
||||||
|
'required_if' => ':attribute Feld ist erforderlich, wenn :other :value ist.',
|
||||||
|
'required_with' => ':attribute Feld ist erforderlich, wenn :values vorhanden ist.',
|
||||||
|
'required_with_all' => ':attribute Feld ist erforderlich, wenn :values vorhanden sind.',
|
||||||
|
'required_without' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden ist.',
|
||||||
|
'required_without_all' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden sind.',
|
||||||
|
'same' => ':attribute und :other muss übereinstimmen.',
|
||||||
|
'size' => [
|
||||||
|
'numeric' => ':attribute muss :size sein.',
|
||||||
|
'file' => ':attribute muss :size Kilobytes groß sein.',
|
||||||
|
'string' => ':attribute muss :size Zeichen lang sein.',
|
||||||
|
'array' => ':attribute muss :size Elemente enthalten.',
|
||||||
|
],
|
||||||
|
'string' => ':attribute muss eine Zeichenkette sein.',
|
||||||
|
'timezone' => ':attribute muss eine valide zeitzone sein.',
|
||||||
|
'unique' => ':attribute wird bereits verwendet.',
|
||||||
|
'url' => ':attribute ist kein valides Format.',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Custom Validation Language Lines
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify custom validation messages for attributes using the
|
||||||
|
| convention "attribute.rule" to name lines. This makes it quick to
|
||||||
|
| specify a specific custom language line for a given attribute rule.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'custom' => [
|
||||||
|
'attribute-name' => [
|
||||||
|
'rule-name' => 'custom-message',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Custom Validation Attributes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| following language lines are used to swap attribute place-holders
|
||||||
|
| with something more reader friendly such as E-Mail Address instead
|
||||||
|
| of "email". This simply helps us make messages a little cleaner.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'attributes' => [],
|
||||||
|
|
||||||
|
];
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Language Lines
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following language lines are used during authentication for various
|
||||||
|
| messages that we need to display to the user. You are free to modify
|
||||||
|
| these language lines according to your application's requirements.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'failed' => 'These credentials do not match our records.',
|
||||||
|
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email Confirmation Text
|
||||||
|
*/
|
||||||
|
'email_confirm_subject' => 'Confirm your email on :appName',
|
||||||
|
'email_confirm_greeting' => 'Thanks for joining :appName!',
|
||||||
|
'email_confirm_text' => 'Please confirm your email address by clicking the button below:',
|
||||||
|
'email_confirm_action' => 'Confirm Email',
|
||||||
|
'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
|
||||||
|
'email_confirm_success' => 'Your email has been confirmed!',
|
||||||
|
'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
|
||||||
|
];
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings text strings
|
||||||
|
* Contains all text strings used in the general settings sections of BookStack
|
||||||
|
* including users and roles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'settings' => 'Settings',
|
||||||
|
'settings_save' => 'Save Settings',
|
||||||
|
|
||||||
|
'app_settings' => 'App Settings',
|
||||||
|
'app_name' => 'Application name',
|
||||||
|
'app_name_desc' => 'This name is shown in the header and any emails.',
|
||||||
|
'app_name_header' => 'Show Application name in header?',
|
||||||
|
'app_public_viewing' => 'Allow public viewing?',
|
||||||
|
'app_secure_images' => 'Enable higher security image uploads?',
|
||||||
|
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
|
||||||
|
'app_editor' => 'Page editor',
|
||||||
|
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
|
||||||
|
'app_custom_html' => 'Custom HTML head content',
|
||||||
|
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
|
||||||
|
'app_logo' => 'Application logo',
|
||||||
|
'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
|
||||||
|
'app_primary_color' => 'Application primary color',
|
||||||
|
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
|
||||||
|
|
||||||
|
'reg_settings' => 'Registration Settings',
|
||||||
|
'reg_allow' => 'Allow registration?',
|
||||||
|
'reg_default_role' => 'Default user role after registration',
|
||||||
|
'reg_confirm_email' => 'Require email confirmation?',
|
||||||
|
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.',
|
||||||
|
'reg_confirm_restrict_domain' => 'Restrict registration to domain',
|
||||||
|
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
|
||||||
|
'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
|
||||||
|
|
||||||
|
];
|
|
@ -39,7 +39,9 @@
|
||||||
@if(setting('app-logo', '') !== 'none')
|
@if(setting('app-logo', '') !== 'none')
|
||||||
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
|
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
|
||||||
@endif
|
@endif
|
||||||
<span class="logo-text">{{ setting('app-name') }}</span>
|
@if (setting('app-name-header'))
|
||||||
|
<span class="logo-text">{{ setting('app-name') }}</span>
|
||||||
|
@endif
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4 col-sm-3 text-center">
|
<div class="col-lg-4 col-sm-3 text-center">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
|
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
|
||||||
<h3 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h3>
|
<h4 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h4>
|
||||||
@if(isset($book->searchSnippet))
|
@if(isset($book->searchSnippet))
|
||||||
<p class="text-muted">{!! $book->searchSnippet !!}</p>
|
<p class="text-muted">{!! $book->searchSnippet !!}</p>
|
||||||
@else
|
@else
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
|
<div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
|
||||||
<h3>
|
<h4>
|
||||||
@if (isset($showPath) && $showPath)
|
@if (isset($showPath) && $showPath)
|
||||||
<a href="{{ $chapter->book->getUrl() }}" class="text-book">
|
<a href="{{ $chapter->book->getUrl() }}" class="text-book">
|
||||||
<i class="zmdi zmdi-book"></i>{{ $chapter->book->name }}
|
<i class="zmdi zmdi-book"></i>{{ $chapter->book->name }}
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
|
<a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
|
||||||
<i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
|
<i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h4>
|
||||||
@if(isset($chapter->searchSnippet))
|
@if(isset($chapter->searchSnippet))
|
||||||
<p class="text-muted">{!! $chapter->searchSnippet !!}</p>
|
<p class="text-muted">{!! $chapter->searchSnippet !!}</p>
|
||||||
@else
|
@else
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
|
<p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
|
||||||
<div class="inset-list">
|
<div class="inset-list">
|
||||||
@foreach($chapter->pages as $page)
|
@foreach($chapter->pages as $page)
|
||||||
<h4 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h4>
|
<h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
|
|
||||||
<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
<meta name="viewport" content="width=device-width"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
|
|
||||||
<title>Confirm Your Email At {{ setting('app-name')}}</title>
|
|
||||||
<style style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 100%;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #348eda;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #FFF;
|
|
||||||
background-color: #348eda;
|
|
||||||
border: solid #348eda;
|
|
||||||
border-width: 10px 20px;
|
|
||||||
line-height: 2;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 10px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #FFF;
|
|
||||||
background-color: #aaa;
|
|
||||||
border: solid #aaa;
|
|
||||||
border-width: 10px 20px;
|
|
||||||
line-height: 2;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 10px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.first {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.padding {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.body-wrap {
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.body-wrap .container {
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
|
||||||
color: #444;
|
|
||||||
margin: 10px 0 10px;
|
|
||||||
line-height: 1.2;
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #888888;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li,
|
|
||||||
ol li {
|
|
||||||
margin-left: 5px;
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: block !important;
|
|
||||||
max-width: 600px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
clear: both !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-wrap .container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body bgcolor="#f6f6f6"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;">
|
|
||||||
<!-- body -->
|
|
||||||
<table class="body-wrap" bgcolor="#f6f6f6"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;">
|
|
||||||
<tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
|
|
||||||
<td class="container" bgcolor="#FFFFFF"
|
|
||||||
style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;display:block!important;max-width:600px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;border-width:1px;border-style:solid;border-color:#f0f0f0;">
|
|
||||||
<!-- content -->
|
|
||||||
<div class="content"
|
|
||||||
style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;">
|
|
||||||
<table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
|
|
||||||
<tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
<h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;color:#444;margin-top:10px;margin-bottom:10px;margin-right:0;margin-left:0;line-height:1.2;font-weight:200;font-size:36px;">
|
|
||||||
Email Confirmation</h1>
|
|
||||||
<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
|
|
||||||
Thanks for joining <a href="{{ baseUrl('/', true) }}">{{ setting('app-name')}}</a>. <br/>
|
|
||||||
Please confirm your email address by clicking the button below.</p>
|
|
||||||
<table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
|
|
||||||
<tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
|
|
||||||
<td class="padding"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;padding-top:10px;padding-bottom:10px;padding-right:0;padding-left:0;">
|
|
||||||
<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
|
|
||||||
<a class="btn-primary" href="{{ baseUrl('/register/confirm/' . $token, true) }}"
|
|
||||||
style="margin-top:0;margin-bottom:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;text-decoration:none;color:#FFF;background-color:#348eda;border-style:solid;border-color:#348eda;border-width:10px 20px;line-height:2;font-weight:bold;margin-right:10px;text-align:center;cursor:pointer;display:inline-block;border-radius:4px;">Confirm
|
|
||||||
Email</a></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!-- /content -->
|
|
||||||
</td>
|
|
||||||
<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<!-- /body -->
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
File diff suppressed because one or more lines are too long
|
@ -25,14 +25,14 @@
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div id="recent-drafts">
|
<div id="recent-drafts">
|
||||||
@if(count($draftPages) > 0)
|
@if(count($draftPages) > 0)
|
||||||
<h3>My Recent Drafts</h3>
|
<h4>My Recent Drafts</h4>
|
||||||
@include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
|
@include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if($signedIn)
|
@if($signedIn)
|
||||||
<h3>My Recently Viewed</h3>
|
<h4>My Recently Viewed</h4>
|
||||||
@else
|
@else
|
||||||
<h3>Recent Books</h3>
|
<h4>Recent Books</h4>
|
||||||
@endif
|
@endif
|
||||||
@include('partials/entity-list', [
|
@include('partials/entity-list', [
|
||||||
'entities' => $recents,
|
'entities' => $recents,
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<h3><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h3>
|
<h4><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h4>
|
||||||
<div id="recently-created-pages">
|
<div id="recently-created-pages">
|
||||||
@include('partials/entity-list', [
|
@include('partials/entity-list', [
|
||||||
'entities' => $recentlyCreatedPages,
|
'entities' => $recentlyCreatedPages,
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h3>
|
<h4><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h4>
|
||||||
<div id="recently-updated-pages">
|
<div id="recently-updated-pages">
|
||||||
@include('partials/entity-list', [
|
@include('partials/entity-list', [
|
||||||
'entities' => $recentlyUpdatedPages,
|
'entities' => $recentlyUpdatedPages,
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-4" id="recent-activity">
|
<div class="col-sm-4" id="recent-activity">
|
||||||
<h3>Recent Activity</h3>
|
<h4>Recent Activity</h4>
|
||||||
@include('partials/activity-list', ['activity' => $activity])
|
@include('partials/activity-list', ['activity' => $activity])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,4 @@
|
||||||
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
|
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
|
||||||
@include('partials/entity-selector-popup')
|
@include('partials/entity-selector-popup')
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@stop
|
@stop
|
|
@ -3,10 +3,13 @@
|
||||||
|
|
||||||
<div class="tabs primary-background-light">
|
<div class="tabs primary-background-light">
|
||||||
<span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
|
<span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
|
||||||
<span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
|
<span toolbox-tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
|
||||||
|
@if(userCan('attachment-create-all'))
|
||||||
|
<span toolbox-tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
|
<div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
|
||||||
<h4>Page Tags</h4>
|
<h4>Page Tags</h4>
|
||||||
<div class="padded tags">
|
<div class="padded tags">
|
||||||
<p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
|
<p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
|
||||||
|
@ -34,4 +37,98 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(userCan('attachment-create-all'))
|
||||||
|
<div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
|
||||||
|
<h4>Attachments</h4>
|
||||||
|
<div class="padded files">
|
||||||
|
|
||||||
|
<div id="file-list" ng-show="!editFile">
|
||||||
|
<p class="muted small">Upload some files or attach some link to display on your page. These are visible in the page sidebar. <span class="secondary">Changes here are saved instantly.</span></p>
|
||||||
|
|
||||||
|
<div tab-container>
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<div tab-button="list" class="tab-item">Attached Items</div>
|
||||||
|
<div tab-button="file" class="tab-item">Upload File</div>
|
||||||
|
<div tab-button="link" class="tab-item">Attach Link</div>
|
||||||
|
</div>
|
||||||
|
<div tab-content="list">
|
||||||
|
<table class="file-table" style="width: 100%;">
|
||||||
|
<tbody ui-sortable="sortOptions" ng-model="files" >
|
||||||
|
<tr ng-repeat="file in files track by $index">
|
||||||
|
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
|
||||||
|
<td>
|
||||||
|
<a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a>
|
||||||
|
<div ng-if="file.deleting">
|
||||||
|
<span class="neg small">Click delete again to confirm you want to delete this attachment.</span>
|
||||||
|
<br>
|
||||||
|
<span class="text-primary small" ng-click="file.deleting=false;">Cancel</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
|
||||||
|
<td width="5"></td>
|
||||||
|
<td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="small muted" ng-if="files.length == 0">
|
||||||
|
No files have been uploaded.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div tab-content="file">
|
||||||
|
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
|
||||||
|
</div>
|
||||||
|
<div tab-content="link" sub-form="attachLinkSubmit(file)">
|
||||||
|
<p class="muted small">You can attach a link if you'd prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="attachment-via-link">Link Name</label>
|
||||||
|
<input type="text" placeholder="Link name" ng-model="file.name">
|
||||||
|
<p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="attachment-via-link">Link to file</label>
|
||||||
|
<input type="text" placeholder="Url of site or file" ng-model="file.link">
|
||||||
|
<p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button pos">Attach</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)">
|
||||||
|
<h5>Edit File</h5>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="attachment-name-edit">File Name</label>
|
||||||
|
<input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
|
||||||
|
<p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div tab-container="@{{ editFile.external ? 'link' : 'file' }}">
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<div tab-button="file" class="tab-item">Upload File</div>
|
||||||
|
<div tab-button="link" class="tab-item">Set Link</div>
|
||||||
|
</div>
|
||||||
|
<div tab-content="file">
|
||||||
|
<drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<div tab-content="link">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="attachment-link-edit">Link to file</label>
|
||||||
|
<input type="text" id="attachment-link-edit" placeholder="Attachment link" ng-model="editFile.link">
|
||||||
|
<p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="button" ng-click="cancelEdit()">Back</button>
|
||||||
|
<button type="submit" class="button pos">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
|
@ -1,7 +1,9 @@
|
||||||
|
|
||||||
<div class="page-editor flex-fill flex" ng-controller="PageEditController" editor-type="{{ setting('app-editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
|
<div class="page-editor flex-fill flex" ng-controller="PageEditController" drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}" editor-type="{{ setting('app-editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
|
||||||
|
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
|
|
||||||
|
{{--Header Bar--}}
|
||||||
<div class="faded-small toolbar">
|
<div class="faded-small toolbar">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -13,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 faded text-center">
|
<div class="col-sm-4 faded text-center">
|
||||||
|
|
||||||
<div dropdown class="dropdown-container draft-display">
|
<div ng-show="draftsEnabled" dropdown class="dropdown-container draft-display">
|
||||||
<a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span> <i class="zmdi zmdi-more-vert"></i></a>
|
<a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span> <i class="zmdi zmdi-more-vert"></i></a>
|
||||||
<i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
|
<i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -48,13 +50,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{--Title input--}}
|
||||||
<div class="title-input page-title clearfix" ng-non-bindable>
|
<div class="title-input page-title clearfix" ng-non-bindable>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
@include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
|
@include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{--Editors--}}
|
||||||
<div class="edit-area flex-fill flex">
|
<div class="edit-area flex-fill flex">
|
||||||
|
|
||||||
|
{{--WYSIWYG Editor--}}
|
||||||
@if(setting('app-editor') === 'wysiwyg')
|
@if(setting('app-editor') === 'wysiwyg')
|
||||||
<div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
|
<div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
|
||||||
<textarea id="html-editor" name="html" rows="5" ng-non-bindable
|
<textarea id="html-editor" name="html" rows="5" ng-non-bindable
|
||||||
|
@ -66,6 +72,7 @@
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{--Markdown Editor--}}
|
||||||
@if(setting('app-editor') === 'markdown')
|
@if(setting('app-editor') === 'markdown')
|
||||||
<div id="markdown-editor" markdown-editor class="flex-fill flex">
|
<div id="markdown-editor" markdown-editor class="flex-fill flex">
|
||||||
|
|
||||||
|
@ -102,7 +109,7 @@
|
||||||
@if($errors->has('markdown'))
|
@if($errors->has('markdown'))
|
||||||
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
@extends('base')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="container small" ng-non-bindable>
|
||||||
|
<h1>Create Page</h1>
|
||||||
|
<form action="{{ $parent->getUrl('/page/create/guest') }}" method="POST">
|
||||||
|
|
||||||
|
{!! csrf_field() !!}
|
||||||
|
|
||||||
|
<div class="form-group title-input">
|
||||||
|
<label for="name">Page Name</label>
|
||||||
|
@include('form/text', ['name' => 'name'])
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<a href="{{ $parent->getUrl() }}" class="button muted">Cancel</a>
|
||||||
|
<button type="submit" class="button pos">Continue</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@stop
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
|
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
|
||||||
<h3>
|
<h4>
|
||||||
<a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
|
<a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
|
||||||
</h3>
|
</h4>
|
||||||
|
|
||||||
@if(isset($page->searchSnippet))
|
@if(isset($page->searchSnippet))
|
||||||
<p class="text-muted">{!! $page->searchSnippet !!}</p>
|
<p class="text-muted">{!! $page->searchSnippet !!}</p>
|
||||||
|
|
|
@ -24,5 +24,9 @@
|
||||||
|
|
||||||
<div style="clear:left;"></div>
|
<div style="clear:left;"></div>
|
||||||
|
|
||||||
{!! $page->html !!}
|
@if (isset($diff) && $diff)
|
||||||
|
{!! $diff !!}
|
||||||
|
@else
|
||||||
|
{!! $page->html !!}
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
|
@ -32,11 +32,11 @@
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<th width="25%">Name</th>
|
<th width="23%">Name</th>
|
||||||
<th colspan="2" width="10%">Created By</th>
|
<th colspan="2" width="8%">Created By</th>
|
||||||
<th width="15%">Revision Date</th>
|
<th width="15%">Revision Date</th>
|
||||||
<th width="25%">Changelog</th>
|
<th width="25%">Changelog</th>
|
||||||
<th width="15%">Actions</th>
|
<th width="20%">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@foreach($page->revisions as $index => $revision)
|
@foreach($page->revisions as $index => $revision)
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -49,15 +49,18 @@
|
||||||
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
|
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
|
||||||
<td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
|
<td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
|
||||||
<td>{{ $revision->summary }}</td>
|
<td>{{ $revision->summary }}</td>
|
||||||
@if ($index !== 0)
|
<td>
|
||||||
<td>
|
<a href="{{ $revision->getUrl('changes') }}" target="_blank">Changes</a>
|
||||||
|
<span class="text-muted"> | </span>
|
||||||
|
|
||||||
|
@if ($index === 0)
|
||||||
|
<a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a>
|
||||||
|
@else
|
||||||
<a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
|
<a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
|
||||||
<span class="text-muted"> | </span>
|
<span class="text-muted"> | </span>
|
||||||
<a href="{{ $revision->getUrl() }}/restore">Restore</a>
|
<a href="{{ $revision->getUrl('restore') }}" target="_blank">Restore</a>
|
||||||
</td>
|
@endif
|
||||||
@else
|
</td>
|
||||||
<td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
|
|
||||||
@endif
|
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</table>
|
</table>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue