commit
d2efc2f47f
|
@ -11,4 +11,5 @@ Homestead.yaml
|
|||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
.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)
|
||||
{
|
||||
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;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -160,44 +160,50 @@ class Entity extends Ownable
|
|||
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$exactTerms = [];
|
||||
if (count($terms) === 0) {
|
||||
$search = $this;
|
||||
$orderBy = 'updated_at';
|
||||
} else {
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/".*?"/', $term)) {
|
||||
$term = str_replace('"', '', $term);
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
$term = '"' . $term . '"';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
}
|
||||
if ($term !== '*') $terms[$key] = $term;
|
||||
}
|
||||
$termString = implode(' ', $terms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
$fuzzyTerms = [];
|
||||
$search = static::newQuery();
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/".*?"/', $term) || is_numeric($term)) {
|
||||
$term = str_replace('"', '', $term);
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
if ($term !== '*') $fuzzyTerms[] = $term;
|
||||
}
|
||||
$orderBy = 'title_relevance';
|
||||
};
|
||||
}
|
||||
|
||||
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
|
||||
|
||||
|
||||
// Perform fulltext search if relevant terms exist.
|
||||
if ($isFuzzy) {
|
||||
$termString = implode(' ', $fuzzyTerms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
}
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if ($this->isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class FileUploadException extends PrettyException {}
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ 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
|
||||
{
|
||||
|
@ -30,4 +32,37 @@ class ForgotPasswordController extends Controller
|
|||
$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)]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
|
|
|
@ -51,7 +51,7 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest');
|
||||
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
|
@ -297,5 +297,4 @@ class RegisterController extends Controller
|
|||
return $this->registerUser($userData, $socialAccount);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -20,6 +20,8 @@ class ResetPasswordController extends Controller
|
|||
|
||||
use ResetsPasswords;
|
||||
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
|
@ -30,4 +32,18 @@ class ResetPasswordController extends Controller
|
|||
$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);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$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->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$chapter->updated_by = user()->id;
|
||||
$chapter->save();
|
||||
Activity::add($chapter, 'chapter_update', $book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
|
|
|
@ -3,13 +3,11 @@
|
|||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Ownable;
|
||||
use HttpRequestException;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Exception\HttpResponseException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use BookStack\User;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
|
@ -33,17 +31,16 @@ abstract class Controller extends BaseController
|
|||
$this->middleware(function ($request, $next) {
|
||||
|
||||
// Get a user instance for the current user
|
||||
$user = auth()->user();
|
||||
if (!$user) $user = User::getDefault();
|
||||
|
||||
// Share variables with views
|
||||
view()->share('signedIn', auth()->check());
|
||||
view()->share('currentUser', $user);
|
||||
$user = user();
|
||||
|
||||
// Share variables with controllers
|
||||
$this->currentUser = $user;
|
||||
$this->signedIn = auth()->check();
|
||||
|
||||
// Share variables with views
|
||||
view()->share('signedIn', $this->signedIn);
|
||||
view()->share('currentUser', $user);
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
@ -72,8 +69,13 @@ abstract class Controller extends BaseController
|
|||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
|
||||
if (request()->wantsJson()) {
|
||||
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
|
||||
} else {
|
||||
$response = redirect('/');
|
||||
session()->flash('error', trans('errors.permission'));
|
||||
}
|
||||
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
|
@ -84,7 +86,7 @@ abstract class Controller extends BaseController
|
|||
*/
|
||||
protected function checkPermission($permissionName)
|
||||
{
|
||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
||||
if (!user() || !user()->can($permissionName)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
return true;
|
||||
|
@ -126,4 +128,22 @@ abstract class Controller extends BaseController
|
|||
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 Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Views;
|
||||
use GatherContent\Htmldiff\Htmldiff;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
|
@ -43,20 +44,53 @@ class PageController extends Controller
|
|||
/**
|
||||
* Show the form for creating a new page.
|
||||
* @param string $bookSlug
|
||||
* @param bool $chapterSlug
|
||||
* @param string $chapterSlug
|
||||
* @return Response
|
||||
* @internal param bool $pageSlug
|
||||
*/
|
||||
public function create($bookSlug, $chapterSlug = false)
|
||||
public function create($bookSlug, $chapterSlug = null)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
$this->setPageTitle('Create New Page');
|
||||
|
||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
// Redirect to draft edit screen if signed in
|
||||
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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,7 +106,13 @@ class PageController extends Controller
|
|||
$this->checkOwnablePermission('page-create', $book);
|
||||
$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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -182,7 +222,13 @@ class PageController extends Controller
|
|||
|
||||
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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -215,6 +261,14 @@ class PageController extends Controller
|
|||
{
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
if (!$this->signedIn) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Guests cannot save drafts',
|
||||
], 500);
|
||||
}
|
||||
|
||||
if ($page->draft) {
|
||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||
} else {
|
||||
|
@ -335,9 +389,41 @@ class PageController extends Controller
|
|||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$revision = $this->pageRepo->getRevisionById($revisionId);
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,10 +17,7 @@ class SettingController extends Controller
|
|||
$this->setPageTitle('Settings');
|
||||
|
||||
// Get application version
|
||||
$version = false;
|
||||
if (function_exists('exec')) {
|
||||
$version = exec('git describe --always --tags ');
|
||||
}
|
||||
$version = trim(file_get_contents(base_path('version')));
|
||||
|
||||
return view('settings/index', ['version' => $version]);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ class UserController extends Controller
|
|||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
$roles = $this->userRepo->getAssignableRoles();
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
|
@ -126,12 +126,13 @@ class UserController extends Controller
|
|||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
|
||||
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
||||
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
$roles = $this->userRepo->getAssignableRoles();
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
|
@ -186,7 +187,7 @@ class UserController extends Controller
|
|||
|
||||
/**
|
||||
* Show the user delete page.
|
||||
* @param $id
|
||||
* @param int $id
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function delete($id)
|
||||
|
@ -219,6 +220,11 @@ class UserController extends Controller
|
|||
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);
|
||||
session()->flash('success', 'User successfully removed');
|
||||
|
||||
|
|
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param string|bool $path
|
||||
|
@ -63,13 +72,13 @@ class Page extends Entity
|
|||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
$midText = $this->draft ? '/draft/' : '/page/';
|
||||
$idComponent = $this->draft ? $this->id : $this->slug;
|
||||
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
|
||||
|
||||
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.
|
||||
* @param null|string $path
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -132,8 +132,8 @@ class BookRepo extends EntityRepo
|
|||
{
|
||||
$book = $this->book->newInstance($input);
|
||||
$book->slug = $this->findSuitableSlug($book->name);
|
||||
$book->created_by = auth()->user()->id;
|
||||
$book->updated_by = auth()->user()->id;
|
||||
$book->created_by = user()->id;
|
||||
$book->updated_by = user()->id;
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
return $book;
|
||||
|
@ -147,9 +147,11 @@ class BookRepo extends EntityRepo
|
|||
*/
|
||||
public function updateFromInput(Book $book, $input)
|
||||
{
|
||||
if ($book->name !== $input['name']) {
|
||||
$book->slug = $this->findSuitableSlug($input['name'], $book->id);
|
||||
}
|
||||
$book->fill($input);
|
||||
$book->slug = $this->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = auth()->user()->id;
|
||||
$book->updated_by = user()->id;
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
return $book;
|
||||
|
@ -208,8 +210,7 @@ class BookRepo extends EntityRepo
|
|||
*/
|
||||
public function findSuitableSlug($name, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
$slug = $this->nameToSlug($name);
|
||||
while ($this->doesSlugExist($slug, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
|
|
|
@ -98,8 +98,8 @@ class ChapterRepo extends EntityRepo
|
|||
{
|
||||
$chapter = $this->chapter->newInstance($input);
|
||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
|
||||
$chapter->created_by = auth()->user()->id;
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$chapter->created_by = user()->id;
|
||||
$chapter->updated_by = user()->id;
|
||||
$chapter = $book->chapters()->save($chapter);
|
||||
$this->permissionService->buildJointPermissionsForEntity($chapter);
|
||||
return $chapter;
|
||||
|
@ -150,8 +150,7 @@ class ChapterRepo extends EntityRepo
|
|||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
$slug = $this->nameToSlug($name);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
|
|
|
@ -132,9 +132,8 @@ class EntityRepo
|
|||
*/
|
||||
public function getUserDraftPages($count = 20, $page = 0)
|
||||
{
|
||||
$user = auth()->user();
|
||||
return $this->page->where('draft', '=', true)
|
||||
->where('created_by', '=', $user->id)
|
||||
->where('created_by', '=', user()->id)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
}
|
||||
|
@ -270,6 +269,19 @@ class EntityRepo
|
|||
$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\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
|
@ -191,7 +192,12 @@ class ImageRepo
|
|||
*/
|
||||
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,6 +5,7 @@ use BookStack\Book;
|
|||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
@ -48,7 +49,7 @@ class PageRepo extends EntityRepo
|
|||
* Get a page via a specific ID.
|
||||
* @param $id
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
* @return Page
|
||||
*/
|
||||
public function getById($id, $allowDrafts = false)
|
||||
{
|
||||
|
@ -59,7 +60,7 @@ class PageRepo extends EntityRepo
|
|||
* Get a page identified by the given slug.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @return Page
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
|
@ -148,8 +149,8 @@ class PageRepo extends EntityRepo
|
|||
{
|
||||
$page = $this->page->newInstance();
|
||||
$page->name = 'New Page';
|
||||
$page->created_by = auth()->user()->id;
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->created_by = user()->id;
|
||||
$page->updated_by = user()->id;
|
||||
$page->draft = true;
|
||||
|
||||
if ($chapter) $page->chapter_id = $chapter->id;
|
||||
|
@ -330,7 +331,7 @@ class PageRepo extends EntityRepo
|
|||
}
|
||||
|
||||
// Update with new details
|
||||
$userId = auth()->user()->id;
|
||||
$userId = user()->id;
|
||||
$page->fill($input);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
|
@ -363,7 +364,7 @@ class PageRepo extends EntityRepo
|
|||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->updated_by = user()->id;
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
@ -381,7 +382,7 @@ class PageRepo extends EntityRepo
|
|||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->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->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
|
@ -404,7 +405,7 @@ class PageRepo extends EntityRepo
|
|||
*/
|
||||
public function saveUpdateDraft(Page $page, $data = [])
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$userId = user()->id;
|
||||
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
|
||||
|
||||
if ($drafts->count() > 0) {
|
||||
|
@ -535,7 +536,7 @@ class PageRepo extends EntityRepo
|
|||
$query = $this->pageRevision->where('type', '=', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->where('updated_at', '>', $page->updated_at)
|
||||
->where('created_by', '!=', auth()->user()->id)
|
||||
->where('created_by', '!=', user()->id)
|
||||
->with('createdBy');
|
||||
|
||||
if ($minRange !== null) {
|
||||
|
@ -548,7 +549,7 @@ class PageRepo extends EntityRepo
|
|||
/**
|
||||
* Gets a single revision via it's id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
* @return PageRevision
|
||||
*/
|
||||
public function getRevisionById($id)
|
||||
{
|
||||
|
@ -613,8 +614,7 @@ class PageRepo extends EntityRepo
|
|||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
$slug = $this->nameToSlug($name);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
|
@ -633,12 +633,20 @@ class PageRepo extends EntityRepo
|
|||
$page->revisions()->delete();
|
||||
$page->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
|
||||
// Delete AttachedFiles
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
foreach ($page->attachments as $attachment) {
|
||||
$attachmentService->deleteFile($attachment);
|
||||
}
|
||||
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($count = 20)
|
||||
{
|
||||
|
@ -648,6 +656,7 @@ class PageRepo extends EntityRepo
|
|||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($count = 20)
|
||||
{
|
||||
|
|
|
@ -35,7 +35,7 @@ class PermissionsRepo
|
|||
*/
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
|
|
|
@ -199,9 +199,9 @@ class UserRepo
|
|||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAssignableRoles()
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->visible();
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,7 +211,7 @@ class UserRepo
|
|||
*/
|
||||
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.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
* @return Role
|
||||
*/
|
||||
public static function getRole($roleName)
|
||||
{
|
||||
|
@ -76,7 +76,7 @@ class Role extends Model
|
|||
/**
|
||||
* Get the role object for the specified system role.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
* @return Role
|
||||
*/
|
||||
public static function getSystemRole($roleName)
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ class ActivityService
|
|||
{
|
||||
$this->activity = $activity;
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
|
@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
|
|||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService
|
||||
class ImageService extends UploadService
|
||||
{
|
||||
|
||||
protected $imageTool;
|
||||
protected $fileSystem;
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @var FileSystemInstance
|
||||
*/
|
||||
protected $storageInstance;
|
||||
protected $storageUrl;
|
||||
|
||||
/**
|
||||
|
@ -34,8 +27,8 @@ class ImageService
|
|||
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
|
||||
{
|
||||
$this->imageTool = $imageTool;
|
||||
$this->fileSystem = $fileSystem;
|
||||
$this->cache = $cache;
|
||||
parent::__construct($fileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +81,9 @@ class ImageService
|
|||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
|
||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
||||
|
||||
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
|
||||
|
||||
while ($storage->exists($imagePath . $imageName)) {
|
||||
$imageName = str_random(3) . $imageName;
|
||||
}
|
||||
|
@ -100,6 +96,8 @@ class ImageService
|
|||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||
}
|
||||
|
||||
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
|
||||
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
|
@ -108,8 +106,8 @@ class ImageService
|
|||
'uploaded_to' => $uploadedTo
|
||||
];
|
||||
|
||||
if (auth()->user() && auth()->user()->id !== 0) {
|
||||
$userId = auth()->user()->id;
|
||||
if (user()->id !== 0) {
|
||||
$userId = user()->id;
|
||||
$imageDetails['created_by'] = $userId;
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
|
@ -119,6 +117,16 @@ class ImageService
|
|||
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.
|
||||
* 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)
|
||||
{
|
||||
$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)) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
|
@ -148,7 +157,7 @@ class ImageService
|
|||
}
|
||||
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
$thumb = $this->imageTool->make($storage->get($imagePath));
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||
|
@ -183,8 +192,8 @@ class ImageService
|
|||
{
|
||||
$storage = $this->getStorage();
|
||||
|
||||
$imageFolder = dirname($image->path);
|
||||
$imageFileName = basename($image->path);
|
||||
$imageFolder = dirname($this->getPath($image));
|
||||
$imageFileName = basename($this->getPath($image));
|
||||
$allImages = collect($storage->allFiles($imageFolder));
|
||||
|
||||
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
|
||||
|
@ -222,35 +231,9 @@ class ImageService
|
|||
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.
|
||||
* @param $filePath
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
private function getPublicUrl($filePath)
|
||||
|
@ -273,6 +256,8 @@ class ImageService
|
|||
$this->storageUrl = $storageUrl;
|
||||
}
|
||||
|
||||
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
|
||||
|
||||
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
}
|
||||
|
||||
|
|
|
@ -614,7 +614,7 @@ class PermissionService
|
|||
private function currentUser()
|
||||
{
|
||||
if ($this->currentUserModel === false) {
|
||||
$this->currentUserModel = auth()->user() ? auth()->user() : new User();
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
return $this->currentUserModel;
|
||||
|
|
|
@ -100,7 +100,7 @@ class SocialAuthService
|
|||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||
$user = $this->userRepo->getByEmail($socialUser->getEmail());
|
||||
$isLoggedIn = auth()->check();
|
||||
$currentUser = auth()->user();
|
||||
$currentUser = user();
|
||||
|
||||
// When a user is not logged in and a matching SocialAccount exists,
|
||||
// Simply log the user into the application.
|
||||
|
@ -214,9 +214,9 @@ class SocialAuthService
|
|||
public function detachSocialAccount($socialDriver)
|
||||
{
|
||||
session();
|
||||
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
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)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->user = user();
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ class ViewService
|
|||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('user_id', '=', auth()->user()->id);
|
||||
$query = $query->where('user_id', '=', user()->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||
|
|
20
app/User.php
20
app/User.php
|
@ -5,6 +5,7 @@ use Illuminate\Auth\Authenticatable;
|
|||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||
|
@ -36,21 +37,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||
protected $permissions;
|
||||
|
||||
/**
|
||||
* Returns a default guest user.
|
||||
* Returns the default public user.
|
||||
* @return User
|
||||
*/
|
||||
public static function getDefault()
|
||||
{
|
||||
return new static([
|
||||
'email' => 'guest',
|
||||
'name' => 'Guest'
|
||||
]);
|
||||
return static::where('system_name', '=', 'public')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
if ($this->id === 0) return ;
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,29 +11,30 @@ use BookStack\Ownable;
|
|||
*/
|
||||
function versioned_asset($file = '')
|
||||
{
|
||||
// Don't require css and JS assets for testing
|
||||
if (config('app.env') === 'testing') return '';
|
||||
static $version = null;
|
||||
|
||||
static $manifest = null;
|
||||
$manifestPath = 'build/manifest.json';
|
||||
|
||||
if (is_null($manifest) && file_exists($manifestPath)) {
|
||||
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
|
||||
} else if (!file_exists($manifestPath)) {
|
||||
if (config('app.env') !== 'production') {
|
||||
$path = public_path($manifestPath);
|
||||
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
|
||||
} else {
|
||||
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
|
||||
}
|
||||
throw new \Exception($error);
|
||||
if (is_null($version)) {
|
||||
$versionFile = base_path('version');
|
||||
$version = trim(file_get_contents($versionFile));
|
||||
}
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return baseUrl($manifest[$file]);
|
||||
$additional = '';
|
||||
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)
|
||||
{
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
return user() && user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
|
@ -128,14 +129,14 @@ function sortUrl($path, $data, $overrideData = [])
|
|||
{
|
||||
$queryStringSections = [];
|
||||
$queryData = array_merge($data, $overrideData);
|
||||
|
||||
|
||||
// Change sorting direction is already sorted on current attribute
|
||||
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
||||
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
||||
} else {
|
||||
$queryData['order'] = 'asc';
|
||||
}
|
||||
|
||||
|
||||
foreach ($queryData as $name => $value) {
|
||||
$trimmedVal = trim($value);
|
||||
if ($trimmedVal === '') continue;
|
||||
|
@ -145,4 +146,4 @@ function sortUrl($path, $data, $overrideData = [])
|
|||
if (count($queryStringSections) === 0) return $path;
|
||||
|
||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
"require": {
|
||||
"php": ">=5.6.4",
|
||||
"laravel/framework": "^5.3.4",
|
||||
"ext-tidy": "*",
|
||||
"intervention/image": "^2.3",
|
||||
"laravel/socialite": "^2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"barryvdh/laravel-debugbar": "^2.2.3",
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "^0.7",
|
||||
"predis/predis": "^1.1"
|
||||
"predis/predis": "^1.1",
|
||||
"gathercontent/htmldiff": "^0.2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
|
|
|
@ -4,21 +4,21 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "c90a6e41767306ceb3b8cedb91468390",
|
||||
"content-hash": "3b5d2d6b77fbe71101e7e8eaff0754fe",
|
||||
"hash": "3124d900cfe857392a94de479f3ff6d4",
|
||||
"content-hash": "a968767a73f77e66e865c276cf76eedf",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.19.6",
|
||||
"version": "3.19.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "34060bf0db260031697b17dbb37fa1bbec92f1c4"
|
||||
"reference": "19bac3bdd7988cbf7f89d5ce8e2748d774e2cde8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/34060bf0db260031697b17dbb37fa1bbec92f1c4",
|
||||
"reference": "34060bf0db260031697b17dbb37fa1bbec92f1c4",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/19bac3bdd7988cbf7f89d5ce8e2748d774e2cde8",
|
||||
"reference": "19bac3bdd7988cbf7f89d5ce8e2748d774e2cde8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -85,32 +85,32 @@
|
|||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"time": "2016-09-08 20:27:15"
|
||||
"time": "2016-09-27 19:38:36"
|
||||
},
|
||||
{
|
||||
"name": "barryvdh/laravel-debugbar",
|
||||
"version": "V2.2.3",
|
||||
"version": "v2.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-debugbar.git",
|
||||
"reference": "ecd1ce5c4a827e2f6a8fb41bcf67713beb1c1cbd"
|
||||
"reference": "0c87981df959c7c1943abe227baf607c92f204f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/ecd1ce5c4a827e2f6a8fb41bcf67713beb1c1cbd",
|
||||
"reference": "ecd1ce5c4a827e2f6a8fb41bcf67713beb1c1cbd",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/0c87981df959c7c1943abe227baf607c92f204f9",
|
||||
"reference": "0c87981df959c7c1943abe227baf607c92f204f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "5.1.*|5.2.*|5.3.*",
|
||||
"maximebf/debugbar": "~1.11.0|~1.12.0",
|
||||
"maximebf/debugbar": "~1.13.0",
|
||||
"php": ">=5.5.9",
|
||||
"symfony/finder": "~2.7|~3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.2-dev"
|
||||
"dev-master": "2.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -139,7 +139,7 @@
|
|||
"profiler",
|
||||
"webprofiler"
|
||||
],
|
||||
"time": "2016-07-29 15:00:36"
|
||||
"time": "2016-09-15 14:05:56"
|
||||
},
|
||||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
|
@ -358,6 +358,57 @@
|
|||
],
|
||||
"time": "2015-11-09 22:51:51"
|
||||
},
|
||||
{
|
||||
"name": "cogpowered/finediff",
|
||||
"version": "0.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cogpowered/FineDiff.git",
|
||||
"reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cogpowered/FineDiff/zipball/339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8",
|
||||
"reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "*",
|
||||
"phpunit/phpunit": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"cogpowered\\FineDiff": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rob Crowe",
|
||||
"email": "rob@cogpowered.com"
|
||||
},
|
||||
{
|
||||
"name": "Raymond Hill"
|
||||
}
|
||||
],
|
||||
"description": "PHP implementation of a Fine granularity Diff engine",
|
||||
"homepage": "https://github.com/cogpowered/FineDiff",
|
||||
"keywords": [
|
||||
"diff",
|
||||
"finediff",
|
||||
"opcode",
|
||||
"string",
|
||||
"text"
|
||||
],
|
||||
"time": "2014-05-19 10:25:02"
|
||||
},
|
||||
{
|
||||
"name": "dnoegel/php-xdg-base-dir",
|
||||
"version": "0.1",
|
||||
|
@ -519,6 +570,55 @@
|
|||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"time": "2016-05-11 00:36:29"
|
||||
},
|
||||
{
|
||||
"name": "gathercontent/htmldiff",
|
||||
"version": "0.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gathercontent/htmldiff.git",
|
||||
"reference": "24674a62315f64330134b4a4c5b01a7b59193c93"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/gathercontent/htmldiff/zipball/24674a62315f64330134b4a4c5b01a7b59193c93",
|
||||
"reference": "24674a62315f64330134b4a4c5b01a7b59193c93",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"cogpowered/finediff": "0.3.1",
|
||||
"ext-tidy": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*",
|
||||
"squizlabs/php_codesniffer": "1.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"GatherContent\\Htmldiff": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andrew Cairns",
|
||||
"email": "andrew@gathercontent.com"
|
||||
},
|
||||
{
|
||||
"name": "Mathew Chapman",
|
||||
"email": "mat@gathercontent.com"
|
||||
},
|
||||
{
|
||||
"name": "Peter Legierski",
|
||||
"email": "peter@gathercontent.com"
|
||||
}
|
||||
],
|
||||
"description": "Compare two HTML strings",
|
||||
"time": "2015-04-15 15:39:46"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "6.2.1",
|
||||
|
@ -899,16 +999,16 @@
|
|||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v5.3.9",
|
||||
"version": "v5.3.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "f6fbb481672f8dc4bc6882d5d654bbfa3588c8ec"
|
||||
"reference": "ca48001b95a0543fb39fcd7219de960bbc03eaa5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/f6fbb481672f8dc4bc6882d5d654bbfa3588c8ec",
|
||||
"reference": "f6fbb481672f8dc4bc6882d5d654bbfa3588c8ec",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/ca48001b95a0543fb39fcd7219de960bbc03eaa5",
|
||||
"reference": "ca48001b95a0543fb39fcd7219de960bbc03eaa5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -956,6 +1056,7 @@
|
|||
"illuminate/http": "self.version",
|
||||
"illuminate/log": "self.version",
|
||||
"illuminate/mail": "self.version",
|
||||
"illuminate/notifications": "self.version",
|
||||
"illuminate/pagination": "self.version",
|
||||
"illuminate/pipeline": "self.version",
|
||||
"illuminate/queue": "self.version",
|
||||
|
@ -1022,7 +1123,7 @@
|
|||
"framework",
|
||||
"laravel"
|
||||
],
|
||||
"time": "2016-09-12 14:08:29"
|
||||
"time": "2016-09-28 02:15:37"
|
||||
},
|
||||
{
|
||||
"name": "laravel/socialite",
|
||||
|
@ -1273,16 +1374,16 @@
|
|||
},
|
||||
{
|
||||
"name": "maximebf/debugbar",
|
||||
"version": "v1.12.0",
|
||||
"version": "v1.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maximebf/php-debugbar.git",
|
||||
"reference": "e634fbd32cd6bc3fa0e8c972b52d4bf49bab3988"
|
||||
"reference": "5f49a5ed6cfde81d31d89378806670d77462526e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/e634fbd32cd6bc3fa0e8c972b52d4bf49bab3988",
|
||||
"reference": "e634fbd32cd6bc3fa0e8c972b52d4bf49bab3988",
|
||||
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/5f49a5ed6cfde81d31d89378806670d77462526e",
|
||||
"reference": "5f49a5ed6cfde81d31d89378806670d77462526e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1301,7 +1402,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.12-dev"
|
||||
"dev-master": "1.13-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -1330,7 +1431,7 @@
|
|||
"debug",
|
||||
"debugbar"
|
||||
],
|
||||
"time": "2016-05-15 13:11:34"
|
||||
"time": "2016-09-15 14:01:59"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
|
@ -1558,16 +1659,16 @@
|
|||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v2.1.0",
|
||||
"version": "v2.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "47b254ea51f1d6d5dc04b9b299e88346bf2369e3"
|
||||
"reference": "4dd659edadffdc2143e4753df655d866dbfeedf0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/47b254ea51f1d6d5dc04b9b299e88346bf2369e3",
|
||||
"reference": "47b254ea51f1d6d5dc04b9b299e88346bf2369e3",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4dd659edadffdc2143e4753df655d866dbfeedf0",
|
||||
"reference": "4dd659edadffdc2143e4753df655d866dbfeedf0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1605,7 +1706,7 @@
|
|||
"parser",
|
||||
"php"
|
||||
],
|
||||
"time": "2016-04-19 13:41:41"
|
||||
"time": "2016-09-16 12:04:44"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
|
@ -1825,22 +1926,30 @@
|
|||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278b"
|
||||
"reference": "5277094ed527a1c4477177d102fe4c53551953e0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b",
|
||||
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278b",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/5277094ed527a1c4477177d102fe4c53551953e0",
|
||||
"reference": "5277094ed527a1c4477177d102fe4c53551953e0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Psr\\Log\\": ""
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "Psr/Log/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
|
@ -1854,12 +1963,13 @@
|
|||
}
|
||||
],
|
||||
"description": "Common interface for logging libraries",
|
||||
"homepage": "https://github.com/php-fig/log",
|
||||
"keywords": [
|
||||
"log",
|
||||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"time": "2012-12-21 11:40:51"
|
||||
"time": "2016-09-19 16:02:08"
|
||||
},
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
|
@ -3167,16 +3277,16 @@
|
|||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "da8529775f14f4fdae33f916eb0cf65f6afbddbc"
|
||||
"reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/da8529775f14f4fdae33f916eb0cf65f6afbddbc",
|
||||
"reference": "da8529775f14f4fdae33f916eb0cf65f6afbddbc",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f",
|
||||
"reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -3205,7 +3315,7 @@
|
|||
"object",
|
||||
"object graph"
|
||||
],
|
||||
"time": "2016-09-06 16:07:05"
|
||||
"time": "2016-09-16 13:37:59"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
|
@ -3661,24 +3771,24 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "5.5.4",
|
||||
"version": "5.5.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "3e6e88e56c912133de6e99b87728cca7ed70c5f5"
|
||||
"reference": "a57126dc681b08289fef6ac96a48e30656f84350"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6e88e56c912133de6e99b87728cca7ed70c5f5",
|
||||
"reference": "3e6e88e56c912133de6e99b87728cca7ed70c5f5",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a57126dc681b08289fef6ac96a48e30656f84350",
|
||||
"reference": "a57126dc681b08289fef6ac96a48e30656f84350",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-reflection": "*",
|
||||
"ext-spl": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"myclabs/deep-copy": "~1.3",
|
||||
"php": "^5.6 || ^7.0",
|
||||
"phpspec/prophecy": "^1.3.1",
|
||||
|
@ -3700,7 +3810,12 @@
|
|||
"conflict": {
|
||||
"phpdocumentor/reflection-docblock": "3.0.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-tidy": "*",
|
||||
"ext-xdebug": "*",
|
||||
"phpunit/php-invoker": "~1.1"
|
||||
},
|
||||
"bin": [
|
||||
|
@ -3735,7 +3850,7 @@
|
|||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2016-08-26 07:11:44"
|
||||
"time": "2016-09-21 14:40:13"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit-mock-objects",
|
||||
|
@ -4524,7 +4639,8 @@
|
|||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=5.6.4"
|
||||
"php": ">=5.6.4",
|
||||
"ext-tidy": "*"
|
||||
},
|
||||
"platform-dev": []
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'locale' => 'en',
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -56,7 +56,7 @@ return [
|
|||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => public_path(),
|
||||
'root' => base_path(),
|
||||
],
|
||||
|
||||
'ftp' => [
|
||||
|
|
|
@ -9,6 +9,8 @@ return [
|
|||
'app-name-header' => true,
|
||||
'app-editor' => 'wysiwyg',
|
||||
'app-color' => '#0288D1',
|
||||
'app-color-light' => 'rgba(21, 101, 192, 0.15)'
|
||||
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
];
|
|
@ -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');
|
||||
|
||||
// Custom extensions
|
||||
var gulp = require('gulp');
|
||||
var Task = elixir.Task;
|
||||
var fs = require('fs');
|
||||
|
||||
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']);
|
||||
elixir(mix => {
|
||||
mix.sass('styles.scss');
|
||||
mix.sass('print-styles.scss');
|
||||
mix.sass('export-styles.scss');
|
||||
mix.browserify('global.js', './public/js/common.js');
|
||||
});
|
||||
|
|
15
package.json
15
package.json
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.9.0"
|
||||
"scripts": {
|
||||
"prod": "gulp --production",
|
||||
"dev": "gulp watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"angular": "^1.5.5",
|
||||
"angular-animate": "^1.5.5",
|
||||
"angular-resource": "^1.5.5",
|
||||
"angular-sanitize": "^1.5.5",
|
||||
"angular-ui-sortable": "^0.14.0",
|
||||
"babel-runtime": "^5.8.29",
|
||||
"bootstrap-sass": "^3.0.0",
|
||||
"angular-ui-sortable": "^0.15.0",
|
||||
"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",
|
||||
"moment": "^2.12.0",
|
||||
"zeroclipboard": "^2.2.0"
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
<env name="AUTH_METHOD" value="standard"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||
<env name="LDAP_VERSION" value="3"/>
|
||||
<env name="STORAGE_TYPE" value="local"/>
|
||||
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GITHUB_APP_SECRET" 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/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/.
|
||||
|
||||
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
||||
* [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)
|
||||
|
||||
## Development & Testing
|
||||
|
@ -29,7 +31,7 @@ php artisan migrate --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
|
||||
|
||||
|
@ -51,3 +53,5 @@ These are the great projects used to help build BookStack:
|
|||
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
||||
* [Marked](https://github.com/chjj/marked)
|
||||
* [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";
|
||||
|
||||
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',
|
||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||
|
@ -17,7 +19,7 @@ module.exports = function (ngApp, events) {
|
|||
$scope.imageDeleteSuccess = false;
|
||||
$scope.uploadedTo = $attrs.uploadedTo;
|
||||
$scope.view = 'all';
|
||||
|
||||
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
|
||||
|
@ -48,7 +50,7 @@ module.exports = function (ngApp, events) {
|
|||
$scope.hasMore = preSearchHasMore;
|
||||
}
|
||||
$scope.cancelSearch = cancelSearch;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param searchTerm
|
||||
*/
|
||||
$scope.searchImages = function() {
|
||||
|
||||
|
@ -196,7 +197,7 @@ module.exports = function (ngApp, events) {
|
|||
$scope.view = viewName;
|
||||
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
|
||||
fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the details of an image.
|
||||
|
@ -205,7 +206,7 @@ module.exports = function (ngApp, events) {
|
|||
$scope.saveImageDetails = function (event) {
|
||||
event.preventDefault();
|
||||
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');
|
||||
}, (response) => {
|
||||
if (response.status === 422) {
|
||||
|
@ -300,15 +301,16 @@ module.exports = function (ngApp, events) {
|
|||
var isEdit = pageId !== 0;
|
||||
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
||||
var isMarkdown = $attrs.editorType === 'markdown';
|
||||
$scope.draftsEnabled = $attrs.draftsEnabled === 'true';
|
||||
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
||||
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
||||
|
||||
// Set inital header draft text
|
||||
// Set initial header draft text
|
||||
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
||||
$scope.draftText = 'Editing Draft'
|
||||
} else {
|
||||
$scope.draftText = 'Editing Page'
|
||||
};
|
||||
}
|
||||
|
||||
var autoSave = false;
|
||||
|
||||
|
@ -317,7 +319,7 @@ module.exports = function (ngApp, events) {
|
|||
html: false
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
if (isEdit && $scope.draftsEnabled) {
|
||||
setTimeout(() => {
|
||||
startAutoSave();
|
||||
}, 1000);
|
||||
|
@ -366,6 +368,7 @@ module.exports = function (ngApp, events) {
|
|||
* Save a draft update into the system via an AJAX request.
|
||||
*/
|
||||
function saveDraft() {
|
||||
if (!$scope.draftsEnabled) return;
|
||||
var data = {
|
||||
name: $('#name').val(),
|
||||
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
|
||||
|
@ -435,7 +438,7 @@ module.exports = function (ngApp, events) {
|
|||
|
||||
const pageId = Number($attrs.pageId);
|
||||
$scope.tags = [];
|
||||
|
||||
|
||||
$scope.sortOptions = {
|
||||
handle: '.handle',
|
||||
items: '> tr',
|
||||
|
@ -458,7 +461,7 @@ module.exports = function (ngApp, events) {
|
|||
* Get all tags for the current book and add into scope.
|
||||
*/
|
||||
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) => {
|
||||
$scope.tags = responseData.data;
|
||||
addEmptyTag();
|
||||
|
@ -527,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 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) {
|
||||
|
||||
/**
|
||||
|
@ -16,7 +12,12 @@ module.exports = function (ngApp, events) {
|
|||
ngApp.directive('toggleSwitch', function () {
|
||||
return {
|
||||
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,
|
||||
link: function (scope, element, attrs) {
|
||||
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
|
||||
|
@ -41,7 +95,22 @@ module.exports = function (ngApp, events) {
|
|||
ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
|
||||
return {
|
||||
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: {
|
||||
name: '@',
|
||||
resizeHeight: '@',
|
||||
|
@ -108,7 +177,11 @@ module.exports = function (ngApp, events) {
|
|||
ngApp.directive('dropZone', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: dropZoneTemplate,
|
||||
template: `
|
||||
<div class="dropzone-container">
|
||||
<div class="dz-message">Drop files or click here to upload</div>
|
||||
</div>
|
||||
`,
|
||||
scope: {
|
||||
uploadUrl: '@',
|
||||
eventSuccess: '=',
|
||||
|
@ -116,6 +189,7 @@ module.exports = function (ngApp, events) {
|
|||
uploadedTo: '@'
|
||||
},
|
||||
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'), {
|
||||
url: scope.uploadUrl,
|
||||
init: function () {
|
||||
|
@ -488,8 +562,8 @@ module.exports = function (ngApp, events) {
|
|||
link: function (scope, elem, attrs) {
|
||||
|
||||
// Get common elements
|
||||
const $buttons = elem.find('[tab-button]');
|
||||
const $content = elem.find('[tab-content]');
|
||||
const $buttons = elem.find('[toolbox-tab-button]');
|
||||
const $content = elem.find('[toolbox-tab-content]');
|
||||
const $toggle = elem.find('[toolbox-toggle]');
|
||||
|
||||
// Handle toolbox toggle click
|
||||
|
@ -501,17 +575,17 @@ module.exports = function (ngApp, events) {
|
|||
function setActive(tabName, openToolbox) {
|
||||
$buttons.removeClass('active');
|
||||
$content.hide();
|
||||
$buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
|
||||
$content.filter(`[tab-content="${tabName}"]`).show();
|
||||
$buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
|
||||
$content.filter(`[toolbox-tab-content="${tabName}"]`).show();
|
||||
if (openToolbox) elem.addClass('open');
|
||||
}
|
||||
|
||||
// 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
|
||||
$buttons.click(function (e) {
|
||||
let name = $(this).attr('tab-button');
|
||||
let name = $(this).attr('toolbox-tab-button');
|
||||
setActive(name, true);
|
||||
});
|
||||
}
|
||||
|
@ -549,7 +623,7 @@ module.exports = function (ngApp, events) {
|
|||
let val = $input.val();
|
||||
let url = $input.attr('autosuggest');
|
||||
let type = $input.attr('autosuggest-type');
|
||||
|
||||
|
||||
// Add name param to request if for a value
|
||||
if (type.toLowerCase() === 'value') {
|
||||
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);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.Events = new EventManager();
|
||||
|
||||
|
||||
var services = require('./services')(ngApp, window.Events);
|
||||
var directives = require('./directives')(ngApp, window.Events);
|
||||
var controllers = require('./controllers')(ngApp, window.Events);
|
||||
// Load in angular specific items
|
||||
import Services from './services';
|
||||
import Directives from './directives';
|
||||
import Controllers from './controllers';
|
||||
Services(ngApp, window.Events);
|
||||
Directives(ngApp, window.Events);
|
||||
Controllers(ngApp, window.Events);
|
||||
|
||||
//Global jQuery Config & Extensions
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
* @param editor - editor instance
|
||||
*/
|
||||
function editorPaste(e, editor) {
|
||||
if (!e.clipboardData) return
|
||||
if (!e.clipboardData) return;
|
||||
let items = e.clipboardData.items;
|
||||
if (!items) return;
|
||||
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 formData = new FormData();
|
||||
|
|
|
@ -135,6 +135,10 @@
|
|||
border-left: 3px solid #BBB;
|
||||
background-color: #EEE;
|
||||
padding: $-s;
|
||||
display: block;
|
||||
> * {
|
||||
display: inline-block;
|
||||
}
|
||||
&:before {
|
||||
font-family: 'Material-Design-Iconic-Font';
|
||||
padding-right: $-s;
|
||||
|
|
|
@ -43,10 +43,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
//body.ie .popup-body {
|
||||
// min-height: 100%;
|
||||
//}
|
||||
|
||||
.corner-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||
min-height: 70vh;
|
||||
}
|
||||
|
||||
#image-manager .dropzone-container {
|
||||
.dropzone-container {
|
||||
position: relative;
|
||||
border: 3px dashed #DDD;
|
||||
}
|
||||
|
@ -456,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||
border-right: 6px solid transparent;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -110,6 +110,8 @@
|
|||
border-left: 0px solid #FFF;
|
||||
background-color: #FFF;
|
||||
&.fixed {
|
||||
background-color: #FFF;
|
||||
z-index: 5;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-left: $-l;
|
||||
|
|
|
@ -71,6 +71,18 @@
|
|||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
// diffs
|
||||
ins,
|
||||
del {
|
||||
text-decoration: none;
|
||||
}
|
||||
ins {
|
||||
background: #dbffdb;
|
||||
}
|
||||
del {
|
||||
background: #FFECEC;
|
||||
}
|
||||
}
|
||||
|
||||
// Page content pointers
|
||||
|
@ -138,7 +150,6 @@
|
|||
background-color: #FFF;
|
||||
border: 1px solid #DDD;
|
||||
right: $-xl*2;
|
||||
z-index: 99;
|
||||
width: 48px;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
|
@ -189,7 +200,7 @@
|
|||
color: #444;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
div[tab-content] {
|
||||
div[toolbox-tab-content] {
|
||||
padding-bottom: 45px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -197,7 +208,7 @@
|
|||
min-height: 0px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
div[tab-content] .padded {
|
||||
div[toolbox-tab-content] .padded {
|
||||
flex: 1;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
@ -216,21 +227,6 @@
|
|||
padding-top: $-s;
|
||||
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 {
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
|
@ -242,36 +238,42 @@
|
|||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
table td, table th {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
[tab-content] {
|
||||
[toolbox-tab-content] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tag-display {
|
||||
margin: $-xl $-m;
|
||||
border: 1px solid #DDD;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
opacity: 0.7;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
//opacity: 0.7;
|
||||
position: relative;
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
tr:first-child td {
|
||||
padding-top: 0;
|
||||
}
|
||||
.heading th {
|
||||
padding: $-xs $-s;
|
||||
color: #333;
|
||||
color: rgba(100, 100, 100, 0.7);
|
||||
border: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
td {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #DDD;
|
||||
border-bottom: 1px solid #EEE;
|
||||
padding: $-xs $-s;
|
||||
color: #444;
|
||||
}
|
||||
tr td:first-child {
|
||||
padding-left:0;
|
||||
}
|
||||
.tag-value {
|
||||
color: #888;
|
||||
}
|
||||
|
|
|
@ -51,4 +51,14 @@ table.list-table {
|
|||
vertical-align: middle;
|
||||
padding: $-xs;
|
||||
}
|
||||
}
|
||||
|
||||
table.file-table {
|
||||
@extend .no-style;
|
||||
td {
|
||||
padding: $-xs;
|
||||
}
|
||||
.ui-sortable-helper {
|
||||
display: table;
|
||||
}
|
||||
}
|
|
@ -193,7 +193,7 @@ p.neg, p .neg, span.neg, .text-neg {
|
|||
p.muted, p .muted, span.muted, .text-muted {
|
||||
color: lighten($text-dark, 26%);
|
||||
&.small, .small {
|
||||
color: lighten($text-dark, 42%);
|
||||
color: lighten($text-dark, 32%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,7 +262,7 @@ ul {
|
|||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
padding-left: $-m * 1.3;
|
||||
padding-left: $-m * 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -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' => [],
|
||||
|
||||
];
|
|
@ -1,5 +1,12 @@
|
|||
@extends('public')
|
||||
|
||||
@section('header-buttons')
|
||||
<a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
|
||||
@if(setting('registration-enabled'))
|
||||
<a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
|
||||
@endif
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
@extends('public')
|
||||
|
||||
@section('header-buttons')
|
||||
<a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
|
||||
@if(setting('registration-enabled'))
|
||||
<a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
|
||||
@endif
|
||||
@stop
|
||||
|
||||
@section('body-class', 'image-cover login')
|
||||
|
||||
@section('content')
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
@include('partials/custom-styles')
|
||||
|
||||
<!-- Custom user content -->
|
||||
@if(setting('app-custom-head', false))
|
||||
@if(setting('app-custom-head'))
|
||||
{!! setting('app-custom-head') !!}
|
||||
@endif
|
||||
</head>
|
||||
|
|
|
@ -23,10 +23,4 @@
|
|||
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
|
||||
@include('partials/entity-selector-popup')
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
@stop
|
|
@ -3,10 +3,13 @@
|
|||
|
||||
<div class="tabs primary-background-light">
|
||||
<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 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>
|
||||
<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>
|
||||
|
@ -34,4 +37,98 @@
|
|||
</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>
|
|
@ -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() }}
|
||||
|
||||
{{--Header Bar--}}
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
@ -13,7 +15,7 @@
|
|||
</div>
|
||||
<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>
|
||||
<i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
|
||||
<ul>
|
||||
|
@ -48,13 +50,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{--Title input--}}
|
||||
<div class="title-input page-title clearfix" ng-non-bindable>
|
||||
<div class="input">
|
||||
@include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{--Editors--}}
|
||||
<div class="edit-area flex-fill flex">
|
||||
|
||||
{{--WYSIWYG Editor--}}
|
||||
@if(setting('app-editor') === 'wysiwyg')
|
||||
<div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
|
||||
<textarea id="html-editor" name="html" rows="5" ng-non-bindable
|
||||
|
@ -66,6 +72,7 @@
|
|||
@endif
|
||||
@endif
|
||||
|
||||
{{--Markdown Editor--}}
|
||||
@if(setting('app-editor') === 'markdown')
|
||||
<div id="markdown-editor" markdown-editor class="flex-fill flex">
|
||||
|
||||
|
@ -102,7 +109,7 @@
|
|||
@if($errors->has('markdown'))
|
||||
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
|
||||
</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
|
|
@ -2,27 +2,11 @@
|
|||
|
||||
<h1 id="bkmrk-page-title" class="float left">{{$page->name}}</h1>
|
||||
|
||||
@if(count($page->tags) > 0)
|
||||
<div class="tag-display float right">
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="text-left heading primary-background-light">
|
||||
<th colspan="2">Page Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($page->tags as $tag)
|
||||
<tr class="tag">
|
||||
<td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
|
||||
@if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div style="clear:left;"></div>
|
||||
|
||||
{!! $page->html !!}
|
||||
@if (isset($diff) && $diff)
|
||||
{!! $diff !!}
|
||||
@else
|
||||
{!! $page->html !!}
|
||||
@endif
|
||||
</div>
|
|
@ -14,7 +14,7 @@
|
|||
table {
|
||||
max-width: 800px !important;
|
||||
font-size: 0.8em;
|
||||
width: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table td {
|
||||
|
|
|
@ -32,11 +32,11 @@
|
|||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th width="25%">Name</th>
|
||||
<th colspan="2" width="10%">Created By</th>
|
||||
<th width="23%">Name</th>
|
||||
<th colspan="2" width="8%">Created By</th>
|
||||
<th width="15%">Revision Date</th>
|
||||
<th width="25%">Changelog</th>
|
||||
<th width="15%">Actions</th>
|
||||
<th width="20%">Actions</th>
|
||||
</tr>
|
||||
@foreach($page->revisions as $index => $revision)
|
||||
<tr>
|
||||
|
@ -49,15 +49,18 @@
|
|||
<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>{{ $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>
|
||||
<span class="text-muted"> | </span>
|
||||
<a href="{{ $revision->getUrl() }}/restore">Restore</a>
|
||||
</td>
|
||||
@else
|
||||
<td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
|
||||
@endif
|
||||
<a href="{{ $revision->getUrl('restore') }}" target="_blank">Restore</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
|
|
|
@ -115,6 +115,8 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav])
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,31 @@
|
|||
|
||||
<div class="book-tree" ng-non-bindable>
|
||||
|
||||
@if(isset($page) && $page->tags->count() > 0)
|
||||
<div class="tag-display">
|
||||
<h6 class="text-muted">Page Tags</h6>
|
||||
<table>
|
||||
<tbody>
|
||||
@foreach($page->tags as $tag)
|
||||
<tr class="tag">
|
||||
<td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
|
||||
@if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (isset($page) && $page->attachments->count() > 0)
|
||||
<h6 class="text-muted">Attachments</h6>
|
||||
@foreach($page->attachments as $attachment)
|
||||
<div class="attachment">
|
||||
<a href="{{ $attachment->getUrl() }}" @if($attachment->external) target="_blank" @endif><i class="zmdi zmdi-{{ $attachment->external ? 'open-in-new' : 'file' }}"></i> {{ $attachment->name }}</a>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@if (isset($pageNav) && $pageNav)
|
||||
<h6 class="text-muted">Page Navigation</h6>
|
||||
<div class="sidebar-page-nav menu">
|
||||
|
@ -10,8 +35,6 @@
|
|||
</li>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
|
||||
@endif
|
||||
|
||||
<h6 class="text-muted">Book Navigation</h6>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
.nav-tabs a.selected, .nav-tabs .tab-item.selected {
|
||||
border-bottom-color: {{ setting('app-color') }};
|
||||
}
|
||||
p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
|
||||
.text-primary, p.primary, p .primary, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
|
||||
color: {{ setting('app-color') }};
|
||||
}
|
||||
</style>
|
|
@ -17,6 +17,11 @@
|
|||
<!-- Scripts -->
|
||||
<script src="{{ baseUrl("/libs/jquery/jquery.min.js?version=2.1.4") }}"></script>
|
||||
@include('partials/custom-styles')
|
||||
|
||||
<!-- Custom user content -->
|
||||
@if(setting('app-custom-head'))
|
||||
{!! setting('app-custom-head') !!}
|
||||
@endif
|
||||
</head>
|
||||
<body class="@yield('body-class')" ng-app="bookStack">
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
<div class="form-group">
|
||||
<label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
|
||||
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
|
||||
@foreach(\BookStack\Role::visible() as $role)
|
||||
@foreach(\BookStack\Role::all() as $role)
|
||||
<option value="{{$role->id}}" data-role-name="{{ $role->name }}"
|
||||
@if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
|
||||
>
|
||||
|
|
|
@ -106,6 +106,19 @@
|
|||
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attachments</td>
|
||||
<td>@include('settings/roles/checkbox', ['permission' => 'attachment-create-all'])</td>
|
||||
<td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-own']) Own</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-all']) All</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-own']) Own</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-all']) All</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p></p>
|
||||
<a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a>
|
||||
@if($authMethod !== 'system')
|
||||
<a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
@if($user->system_name == 'public')
|
||||
<p>This user represents any guest users that visit your instance. It cannot be used for logins but is assigned automatically.</p>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
@include('form.text', ['name' => 'name'])
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@include('form.text', ['name' => 'email'])
|
||||
</div>
|
||||
|
||||
@if(userCan('users-manage'))
|
||||
<div class="form-group">
|
||||
<label for="role">User Role</label>
|
||||
@include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<a href="{{ baseUrl("/settings/users") }}" class="button muted">Cancel</a>
|
||||
<button class="button pos" type="submit">Save</button>
|
||||
</div>
|
|
@ -27,6 +27,7 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
Route::post('/{bookSlug}/page/create/guest', 'PageController@createAsGuest');
|
||||
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
|
||||
Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
|
||||
|
@ -47,10 +48,12 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
// Revisions
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
|
||||
|
||||
// Chapters
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
|
||||
Route::post('/{bookSlug}/chapter/{chapterSlug}/page/create/guest', 'PageController@createAsGuest');
|
||||
Route::get('/{bookSlug}/chapter/create', 'ChapterController@create');
|
||||
Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
|
||||
|
@ -84,6 +87,16 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
Route::delete('/{imageId}', 'ImageController@destroy');
|
||||
});
|
||||
|
||||
// Attachments routes
|
||||
Route::get('/attachments/{id}', 'AttachmentController@get');
|
||||
Route::post('/attachments/upload', 'AttachmentController@upload');
|
||||
Route::post('/attachments/upload/{id}', 'AttachmentController@uploadUpdate');
|
||||
Route::post('/attachments/link', 'AttachmentController@attachLink');
|
||||
Route::put('/attachments/{id}', 'AttachmentController@update');
|
||||
Route::get('/attachments/get/page/{pageId}', 'AttachmentController@listForPage');
|
||||
Route::put('/attachments/sort/page/{pageId}', 'AttachmentController@sortForPage');
|
||||
Route::delete('/attachments/{id}', 'AttachmentController@delete');
|
||||
|
||||
// AJAX routes
|
||||
Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
|
||||
Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
|
||||
|
@ -140,7 +153,7 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
});
|
||||
|
||||
// Social auth routes
|
||||
Route::get('/login/service/{socialDriver}', 'Auth\RegisterController@getSocialLogin');
|
||||
Route::get('/login/service/{socialDriver}', 'Auth\LoginController@getSocialLogin');
|
||||
Route::get('/login/service/{socialDriver}/callback', 'Auth\RegisterController@socialCallback');
|
||||
Route::get('/login/service/{socialDriver}/detach', 'Auth\RegisterController@detachSocialAccount');
|
||||
Route::get('/register/service/{socialDriver}', 'Auth\RegisterController@socialRegister');
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
|
||||
class AttachmentTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Get a test file that can be uploaded
|
||||
* @param $fileName
|
||||
* @return \Illuminate\Http\UploadedFile
|
||||
*/
|
||||
protected function getTestFile($fileName)
|
||||
{
|
||||
return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file with the given name.
|
||||
* @param $name
|
||||
* @param int $uploadedTo
|
||||
* @return string
|
||||
*/
|
||||
protected function uploadFile($name, $uploadedTo = 0)
|
||||
{
|
||||
$file = $this->getTestFile($name);
|
||||
return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected upload path for a file.
|
||||
* @param $fileName
|
||||
* @return string
|
||||
*/
|
||||
protected function getUploadPath($fileName)
|
||||
{
|
||||
return 'uploads/files/' . Date('Y-m-M') . '/' . $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all uploaded files.
|
||||
* To assist with cleanup.
|
||||
*/
|
||||
protected function deleteUploads()
|
||||
{
|
||||
$fileService = $this->app->make(\BookStack\Services\AttachmentService::class);
|
||||
foreach (\BookStack\Attachment::all() as $file) {
|
||||
$fileService->deleteFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_file_upload()
|
||||
{
|
||||
$page = \BookStack\Page::first();
|
||||
$this->asAdmin();
|
||||
$admin = $this->getAdmin();
|
||||
$fileName = 'upload_test_file.txt';
|
||||
|
||||
$expectedResp = [
|
||||
'name' => $fileName,
|
||||
'uploaded_to'=> $page->id,
|
||||
'extension' => 'txt',
|
||||
'order' => 1,
|
||||
'created_by' => $admin->id,
|
||||
'updated_by' => $admin->id,
|
||||
'path' => $this->getUploadPath($fileName)
|
||||
];
|
||||
|
||||
$this->uploadFile($fileName, $page->id);
|
||||
$this->assertResponseOk();
|
||||
$this->seeJsonContains($expectedResp);
|
||||
$this->seeInDatabase('attachments', $expectedResp);
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
|
||||
public function test_file_display_and_access()
|
||||
{
|
||||
$page = \BookStack\Page::first();
|
||||
$this->asAdmin();
|
||||
$admin = $this->getAdmin();
|
||||
$fileName = 'upload_test_file.txt';
|
||||
|
||||
$this->uploadFile($fileName, $page->id);
|
||||
$this->assertResponseOk();
|
||||
$this->visit($page->getUrl())
|
||||
->seeLink($fileName)
|
||||
->click($fileName)
|
||||
->see('Hi, This is a test file for testing the upload process.');
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
|
||||
public function test_attaching_link_to_page()
|
||||
{
|
||||
$page = \BookStack\Page::first();
|
||||
$admin = $this->getAdmin();
|
||||
$this->asAdmin();
|
||||
|
||||
$this->call('POST', 'attachments/link', [
|
||||
'link' => 'https://example.com',
|
||||
'name' => 'Example Attachment Link',
|
||||
'uploaded_to' => $page->id,
|
||||
]);
|
||||
|
||||
$expectedResp = [
|
||||
'path' => 'https://example.com',
|
||||
'name' => 'Example Attachment Link',
|
||||
'uploaded_to' => $page->id,
|
||||
'created_by' => $admin->id,
|
||||
'updated_by' => $admin->id,
|
||||
'external' => true,
|
||||
'order' => 1,
|
||||
'extension' => ''
|
||||
];
|
||||
|
||||
$this->assertResponseOk();
|
||||
$this->seeJsonContains($expectedResp);
|
||||
$this->seeInDatabase('attachments', $expectedResp);
|
||||
|
||||
$this->visit($page->getUrl())->seeLink('Example Attachment Link')
|
||||
->click('Example Attachment Link')->seePageIs('https://example.com');
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
|
||||
public function test_attachment_updating()
|
||||
{
|
||||
$page = \BookStack\Page::first();
|
||||
$this->asAdmin();
|
||||
|
||||
$this->call('POST', 'attachments/link', [
|
||||
'link' => 'https://example.com',
|
||||
'name' => 'Example Attachment Link',
|
||||
'uploaded_to' => $page->id,
|
||||
]);
|
||||
|
||||
$attachmentId = \BookStack\Attachment::first()->id;
|
||||
|
||||
$this->call('PUT', 'attachments/' . $attachmentId, [
|
||||
'uploaded_to' => $page->id,
|
||||
'name' => 'My new attachment name',
|
||||
'link' => 'https://test.example.com'
|
||||
]);
|
||||
|
||||
$expectedResp = [
|
||||
'path' => 'https://test.example.com',
|
||||
'name' => 'My new attachment name',
|
||||
'uploaded_to' => $page->id
|
||||
];
|
||||
|
||||
$this->assertResponseOk();
|
||||
$this->seeJsonContains($expectedResp);
|
||||
$this->seeInDatabase('attachments', $expectedResp);
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
|
||||
public function test_file_deletion()
|
||||
{
|
||||
$page = \BookStack\Page::first();
|
||||
$this->asAdmin();
|
||||
$fileName = 'deletion_test.txt';
|
||||
$this->uploadFile($fileName, $page->id);
|
||||
|
||||
$filePath = base_path('storage/' . $this->getUploadPath($fileName));
|
||||
|
||||
$this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
|
||||
|
||||
$attachmentId = \BookStack\Attachment::first()->id;
|
||||
$this->call('DELETE', 'attachments/' . $attachmentId);
|
||||
|
||||
$this->dontSeeInDatabase('attachments', [
|
||||
'name' => $fileName
|
||||
]);
|
||||
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
|
||||
public function test_attachment_deletion_on_page_deletion()
|
||||
{
|
||||
$page = \BookStack\Page::first();
|
||||
$this->asAdmin();
|
||||
$fileName = 'deletion_test.txt';
|
||||
$this->uploadFile($fileName, $page->id);
|
||||
|
||||
$filePath = base_path('storage/' . $this->getUploadPath($fileName));
|
||||
|
||||
$this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
|
||||
$this->seeInDatabase('attachments', [
|
||||
'name' => $fileName
|
||||
]);
|
||||
|
||||
$this->call('DELETE', $page->getUrl());
|
||||
|
||||
$this->dontSeeInDatabase('attachments', [
|
||||
'name' => $fileName
|
||||
]);
|
||||
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
}
|
|
@ -146,7 +146,7 @@ class AuthTest extends TestCase
|
|||
|
||||
public function test_user_updating()
|
||||
{
|
||||
$user = \BookStack\User::all()->last();
|
||||
$user = $this->getNormalUser();
|
||||
$password = $user->password;
|
||||
$this->asAdmin()
|
||||
->visit('/settings/users')
|
||||
|
@ -162,7 +162,7 @@ class AuthTest extends TestCase
|
|||
|
||||
public function test_user_password_update()
|
||||
{
|
||||
$user = \BookStack\User::all()->last();
|
||||
$user = $this->getNormalUser();
|
||||
$userProfilePage = '/settings/users/' . $user->id;
|
||||
$this->asAdmin()
|
||||
->visit($userProfilePage)
|
||||
|
@ -218,6 +218,37 @@ class AuthTest extends TestCase
|
|||
->seePageIs('/login');
|
||||
}
|
||||
|
||||
public function test_reset_password_flow()
|
||||
{
|
||||
$this->visit('/login')->click('Forgot Password?')
|
||||
->seePageIs('/password/email')
|
||||
->type('admin@admin.com', 'email')
|
||||
->press('Send Reset Link')
|
||||
->see('A password reset link has been sent to admin@admin.com');
|
||||
|
||||
$this->seeInDatabase('password_resets', [
|
||||
'email' => 'admin@admin.com'
|
||||
]);
|
||||
|
||||
$reset = DB::table('password_resets')->where('email', '=', 'admin@admin.com')->first();
|
||||
$this->visit('/password/reset/' . $reset->token)
|
||||
->see('Reset Password')
|
||||
->submitForm('Reset Password', [
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => 'randompass',
|
||||
'password_confirmation' => 'randompass'
|
||||
])->seePageIs('/')
|
||||
->see('Your password has been successfully reset');
|
||||
}
|
||||
|
||||
public function test_reset_password_page_shows_sign_links()
|
||||
{
|
||||
$this->setSettings(['registration-enabled' => 'true']);
|
||||
$this->visit('/password/email')
|
||||
->seeLink('Sign in')
|
||||
->seeLink('Sign up');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a login
|
||||
* @param string $email
|
||||
|
|
|
@ -108,7 +108,7 @@ class LdapTest extends \TestCase
|
|||
|
||||
public function test_user_edit_form()
|
||||
{
|
||||
$editUser = User::all()->last();
|
||||
$editUser = $this->getNormalUser();
|
||||
$this->asAdmin()->visit('/settings/users/' . $editUser->id)
|
||||
->see('Edit User')
|
||||
->dontSee('Password')
|
||||
|
@ -126,7 +126,7 @@ class LdapTest extends \TestCase
|
|||
|
||||
public function test_non_admins_cannot_change_auth_id()
|
||||
{
|
||||
$testUser = User::all()->last();
|
||||
$testUser = $this->getNormalUser();
|
||||
$this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
|
||||
->dontSee('External Authentication');
|
||||
}
|
||||
|
|
|
@ -91,6 +91,45 @@ class EntitySearchTest extends TestCase
|
|||
->see('Book Search Results')->see('.entity-list', $book->name);
|
||||
}
|
||||
|
||||
public function test_searching_hypen_doesnt_break()
|
||||
{
|
||||
$this->visit('/search/all?term=cat+-')
|
||||
->seeStatusCode(200);
|
||||
}
|
||||
|
||||
public function test_tag_search()
|
||||
{
|
||||
$newTags = [
|
||||
new \BookStack\Tag([
|
||||
'name' => 'animal',
|
||||
'value' => 'cat'
|
||||
]),
|
||||
new \BookStack\Tag([
|
||||
'name' => 'color',
|
||||
'value' => 'red'
|
||||
])
|
||||
];
|
||||
|
||||
$pageA = \BookStack\Page::first();
|
||||
$pageA->tags()->saveMany($newTags);
|
||||
|
||||
$pageB = \BookStack\Page::all()->last();
|
||||
$pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
|
||||
|
||||
$this->asAdmin()->visit('/search/all?term=%5Banimal%5D')
|
||||
->seeLink($pageA->name)
|
||||
->seeLink($pageB->name);
|
||||
|
||||
$this->visit('/search/all?term=%5Bcolor%5D')
|
||||
->seeLink($pageA->name)
|
||||
->dontSeeLink($pageB->name);
|
||||
|
||||
$this->visit('/search/all?term=%5Banimal%3Dcat%5D')
|
||||
->seeLink($pageA->name)
|
||||
->dontSeeLink($pageB->name);
|
||||
|
||||
}
|
||||
|
||||
public function test_ajax_entity_search()
|
||||
{
|
||||
$page = \BookStack\Page::all()->last();
|
||||
|
|
|
@ -10,7 +10,7 @@ class ImageTest extends TestCase
|
|||
*/
|
||||
protected function getTestImage($fileName)
|
||||
{
|
||||
return new \Illuminate\Http\UploadedFile(base_path('tests/test-image.jpg'), $fileName, 'image/jpeg', 5238);
|
||||
return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-image.jpg'), $fileName, 'image/jpeg', 5238);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,7 +57,7 @@ class ImageTest extends TestCase
|
|||
$relPath = $this->uploadImage($imageName, $page->id);
|
||||
$this->assertResponseOk();
|
||||
|
||||
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists');
|
||||
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath));
|
||||
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
|
@ -70,7 +70,6 @@ class ImageTest extends TestCase
|
|||
'updated_by' => $admin->id,
|
||||
'name' => $imageName
|
||||
]);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -544,27 +544,38 @@ class RolesTest extends TestCase
|
|||
->dontSeeInElement('.book-content', $otherPage->name);
|
||||
}
|
||||
|
||||
public function test_public_role_not_visible_in_user_edit_screen()
|
||||
public function test_public_role_visible_in_user_edit_screen()
|
||||
{
|
||||
$user = \BookStack\User::first();
|
||||
$this->asAdmin()->visit('/settings/users/' . $user->id)
|
||||
->seeElement('#roles-admin')
|
||||
->dontSeeElement('#roles-public');
|
||||
->seeElement('#roles-public');
|
||||
}
|
||||
|
||||
public function test_public_role_not_visible_in_role_listing()
|
||||
public function test_public_role_visible_in_role_listing()
|
||||
{
|
||||
$this->asAdmin()->visit('/settings/roles')
|
||||
->see('Admin')
|
||||
->dontSee('Public');
|
||||
->see('Public');
|
||||
}
|
||||
|
||||
public function test_public_role_not_visible_in_default_role_setting()
|
||||
public function test_public_role_visible_in_default_role_setting()
|
||||
{
|
||||
$this->asAdmin()->visit('/settings')
|
||||
->seeElement('[data-role-name="admin"]')
|
||||
->dontSeeElement('[data-role-name="public"]');
|
||||
->seeElement('[data-role-name="public"]');
|
||||
|
||||
}
|
||||
|
||||
public function test_public_role_not_deleteable()
|
||||
{
|
||||
$this->asAdmin()->visit('/settings/roles')
|
||||
->click('Public')
|
||||
->see('Edit Role')
|
||||
->click('Delete Role')
|
||||
->press('Confirm')
|
||||
->see('Delete Role')
|
||||
->see('Cannot be deleted');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
class PublicActionTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_app_not_public()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'false']);
|
||||
$book = \BookStack\Book::orderBy('name', 'asc')->first();
|
||||
$this->visit('/books')->seePageIs('/login');
|
||||
$this->visit($book->getUrl())->seePageIs('/login');
|
||||
|
||||
$page = \BookStack\Page::first();
|
||||
$this->visit($page->getUrl())->seePageIs('/login');
|
||||
}
|
||||
|
||||
public function test_books_viewable()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get();
|
||||
$bookToVisit = $books[1];
|
||||
|
||||
// Check books index page is showing
|
||||
$this->visit('/books')
|
||||
->seeStatusCode(200)
|
||||
->see($books[0]->name)
|
||||
// Check individual book page is showing and it's child contents are visible.
|
||||
->click($bookToVisit->name)
|
||||
->seePageIs($bookToVisit->getUrl())
|
||||
->see($bookToVisit->name)
|
||||
->see($bookToVisit->chapters()->first()->name);
|
||||
}
|
||||
|
||||
public function test_chapters_viewable()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$chapterToVisit = \BookStack\Chapter::first();
|
||||
$pageToVisit = $chapterToVisit->pages()->first();
|
||||
|
||||
// Check chapters index page is showing
|
||||
$this->visit($chapterToVisit->getUrl())
|
||||
->seeStatusCode(200)
|
||||
->see($chapterToVisit->name)
|
||||
// Check individual chapter page is showing and it's child contents are visible.
|
||||
->see($pageToVisit->name)
|
||||
->click($pageToVisit->name)
|
||||
->see($chapterToVisit->book->name)
|
||||
->see($chapterToVisit->name)
|
||||
->seePageIs($pageToVisit->getUrl());
|
||||
}
|
||||
|
||||
public function test_public_page_creation()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$publicRole = \BookStack\Role::getSystemRole('public');
|
||||
// Grant all permissions to public
|
||||
$publicRole->permissions()->detach();
|
||||
foreach (\BookStack\RolePermission::all() as $perm) {
|
||||
$publicRole->attachPermission($perm);
|
||||
}
|
||||
$this->app[\BookStack\Services\PermissionService::class]->buildJointPermissionForRole($publicRole);
|
||||
|
||||
$chapter = \BookStack\Chapter::first();
|
||||
$this->visit($chapter->book->getUrl());
|
||||
$this->visit($chapter->getUrl())
|
||||
->click('New Page')
|
||||
->see('Create Page')
|
||||
->seePageIs($chapter->getUrl('/create-page'));
|
||||
|
||||
$this->submitForm('Continue', [
|
||||
'name' => 'My guest page'
|
||||
])->seePageIs($chapter->book->getUrl('/page/my-guest-page/edit'));
|
||||
|
||||
$user = \BookStack\User::getDefault();
|
||||
$this->seeInDatabase('pages', [
|
||||
'name' => 'My guest page',
|
||||
'chapter_id' => $chapter->id,
|
||||
'created_by' => $user->id,
|
||||
'updated_by' => $user->id
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
class PublicViewTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_books_viewable()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get();
|
||||
$bookToVisit = $books[1];
|
||||
|
||||
// Check books index page is showing
|
||||
$this->visit('/books')
|
||||
->seeStatusCode(200)
|
||||
->see($books[0]->name)
|
||||
// Check individual book page is showing and it's child contents are visible.
|
||||
->click($bookToVisit->name)
|
||||
->seePageIs($bookToVisit->getUrl())
|
||||
->see($bookToVisit->name)
|
||||
->see($bookToVisit->chapters()->first()->name);
|
||||
}
|
||||
|
||||
public function test_chapters_viewable()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$chapterToVisit = \BookStack\Chapter::first();
|
||||
$pageToVisit = $chapterToVisit->pages()->first();
|
||||
|
||||
// Check chapters index page is showing
|
||||
$this->visit($chapterToVisit->getUrl())
|
||||
->seeStatusCode(200)
|
||||
->see($chapterToVisit->name)
|
||||
// Check individual chapter page is showing and it's child contents are visible.
|
||||
->see($pageToVisit->name)
|
||||
->click($pageToVisit->name)
|
||||
->see($chapterToVisit->book->name)
|
||||
->see($chapterToVisit->name)
|
||||
->seePageIs($pageToVisit->getUrl());
|
||||
}
|
||||
|
||||
}
|
|
@ -66,6 +66,14 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
|
|||
return $this->actingAs($this->editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user that's not a system user such as the guest user.
|
||||
*/
|
||||
public function getNormalUser()
|
||||
{
|
||||
return \BookStack\User::where('system_name', '=', null)->get()->last();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly sets an array of settings.
|
||||
* @param $settingsArray
|
||||
|
|
|
@ -76,5 +76,23 @@ class UserProfileTest extends TestCase
|
|||
->seePageIs('/user/' . $newUser->id)
|
||||
->see($newUser->name);
|
||||
}
|
||||
|
||||
public function test_guest_profile_shows_limited_form()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/settings/users')
|
||||
->click('Guest')
|
||||
->dontSeeElement('#password');
|
||||
}
|
||||
|
||||
public function test_guest_profile_cannot_be_deleted()
|
||||
{
|
||||
$guestUser = \BookStack\User::getDefault();
|
||||
$this->asAdmin()->visit('/settings/users/' . $guestUser->id . '/delete')
|
||||
->see('Delete User')->see('Guest')
|
||||
->press('Confirm')
|
||||
->seePageIs('/settings/users/' . $guestUser->id)
|
||||
->see('cannot delete the guest user');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Hi, This is a test file for testing the upload process.
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Loading…
Reference in New Issue