diff --git a/.gitignore b/.gitignore
index 7417bbdd8..362df57e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,5 @@ Homestead.yaml
/storage/images
_ide_helper.php
/storage/debugbar
-.phpstorm.meta.php
\ No newline at end of file
+.phpstorm.meta.php
+yarn.lock
diff --git a/app/Attachment.php b/app/Attachment.php
new file mode 100644
index 000000000..fe291bec2
--- /dev/null
+++ b/app/Attachment.php
@@ -0,0 +1,36 @@
+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);
+ }
+
+}
diff --git a/app/Book.php b/app/Book.php
index aa2dee9c0..91f74ca64 100644
--- a/app/Book.php
+++ b/app/Book.php
@@ -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));
}
/*
diff --git a/app/Chapter.php b/app/Chapter.php
index 8f0453172..cc5518b7a 100644
--- a/app/Chapter.php
+++ b/app/Chapter.php
@@ -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));
}
/**
diff --git a/app/Entity.php b/app/Entity.php
index 496d20a33..186059f00 100644
--- a/app/Entity.php
+++ b/app/Entity.php
@@ -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');
diff --git a/app/Exceptions/FileUploadException.php b/app/Exceptions/FileUploadException.php
new file mode 100644
index 000000000..af976072a
--- /dev/null
+++ b/app/Exceptions/FileUploadException.php
@@ -0,0 +1,4 @@
+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']);
+ }
+}
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
index d93854e23..45e40e6fe 100644
--- a/app/Http/Controllers/Auth/ForgotPasswordController.php
+++ b/app/Http/Controllers/Auth/ForgotPasswordController.php
@@ -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)]
+ );
+ }
+
}
\ No newline at end of file
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index 0de4a8282..c9d6a5496 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -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;
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index 6bba6de04..d9bb500b4 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -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);
}
-
}
\ No newline at end of file
diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php
index 656b8cc42..bd64793f9 100644
--- a/app/Http/Controllers/Auth/ResetPasswordController.php
+++ b/app/Http/Controllers/Auth/ResetPasswordController.php
@@ -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));
+ }
}
\ No newline at end of file
diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php
index 03ec2c110..a3fb600fd 100644
--- a/app/Http/Controllers/ChapterController.php
+++ b/app/Http/Controllers/ChapterController.php
@@ -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());
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 43292d941..2b6c88fe0 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -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());
+ }
+
}
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index 3d6abe5b4..c2d8e257c 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -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,
+ ]);
}
/**
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index 61ce55fa9..65135eda3 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -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]);
}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 4c56516dc..18ef1a671 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -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');
diff --git a/app/Page.php b/app/Page.php
index 1961a4f7f..3ee9e90f4 100644
--- a/app/Page.php
+++ b/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);
}
/**
diff --git a/app/PageRevision.php b/app/PageRevision.php
index 1ffd63dbd..ff469f0ed 100644
--- a/app/PageRevision.php
+++ b/app/PageRevision.php
@@ -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;
}
}
diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php
index fdc4dd8d4..7bb91f472 100644
--- a/app/Repos/BookRepo.php
+++ b/app/Repos/BookRepo.php
@@ -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);
}
diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php
index c12a9f0f2..4c13b9aaf 100644
--- a/app/Repos/ChapterRepo.php
+++ b/app/Repos/ChapterRepo.php
@@ -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);
}
diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php
index c94601738..7ecfb758c 100644
--- a/app/Repos/EntityRepo.php
+++ b/app/Repos/EntityRepo.php
@@ -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;
+ }
+
}
diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php
index 435b8bbd7..8ddde7b0f 100644
--- a/app/Repos/ImageRepo.php
+++ b/app/Repos/ImageRepo.php
@@ -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 [];
+ }
}
diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php
index c64da1267..e6d713f77 100644
--- a/app/Repos/PageRepo.php
+++ b/app/Repos/PageRepo.php
@@ -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)
{
diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php
index e026d83e8..24497c911 100644
--- a/app/Repos/PermissionsRepo.php
+++ b/app/Repos/PermissionsRepo.php
@@ -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);
diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php
index 127db9fb5..ab3716fca 100644
--- a/app/Repos/UserRepo.php
+++ b/app/Repos/UserRepo.php
@@ -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();
}
}
\ No newline at end of file
diff --git a/app/Role.php b/app/Role.php
index 8d0a79e75..bf9685ee2 100644
--- a/app/Role.php
+++ b/app/Role.php
@@ -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)
{
diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php
index f6fea33a1..e41036238 100644
--- a/app/Services/ActivityService.php
+++ b/app/Services/ActivityService.php
@@ -19,7 +19,7 @@ class ActivityService
{
$this->activity = $activity;
$this->permissionService = $permissionService;
- $this->user = auth()->user();
+ $this->user = user();
}
/**
diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php
new file mode 100644
index 000000000..e0ee3a04d
--- /dev/null
+++ b/app/Services/AttachmentService.php
@@ -0,0 +1,201 @@
+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;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php
index aa1375487..dfe2cf453 100644
--- a/app/Services/ImageService.php
+++ b/app/Services/ImageService.php
@@ -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;
}
diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php
index 341a69edb..bb78f0b0a 100644
--- a/app/Services/PermissionService.php
+++ b/app/Services/PermissionService.php
@@ -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;
diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php
index b28a97ea4..d76a7231b 100644
--- a/app/Services/SocialAuthService.php
+++ b/app/Services/SocialAuthService.php
@@ -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());
}
}
\ No newline at end of file
diff --git a/app/Services/UploadService.php b/app/Services/UploadService.php
new file mode 100644
index 000000000..44d4bb4f7
--- /dev/null
+++ b/app/Services/UploadService.php
@@ -0,0 +1,64 @@
+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';
+ }
+}
\ No newline at end of file
diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php
index aac9831f7..1a9ee5f70 100644
--- a/app/Services/ViewService.php
+++ b/app/Services/ViewService.php
@@ -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');
diff --git a/app/User.php b/app/User.php
index 8c39d81be..09b189cbb 100644
--- a/app/User.php
+++ b/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);
}
diff --git a/app/helpers.php b/app/helpers.php
index dd835fbf6..b5be0fd11 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -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));
-}
\ No newline at end of file
+}
diff --git a/composer.json b/composer.json
index d96037019..7d4b5e62b 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/composer.lock b/composer.lock
index c1c80e100..74a090288 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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": []
}
diff --git a/config/app.php b/config/app.php
index a5b0d2fe0..786f005ac 100644
--- a/config/app.php
+++ b/config/app.php
@@ -57,7 +57,7 @@ return [
|
*/
- 'locale' => 'en',
+ 'locale' => env('APP_LANG', 'en'),
/*
|--------------------------------------------------------------------------
diff --git a/config/filesystems.php b/config/filesystems.php
index dbcb03db1..836f68d3d 100644
--- a/config/filesystems.php
+++ b/config/filesystems.php
@@ -56,7 +56,7 @@ return [
'local' => [
'driver' => 'local',
- 'root' => public_path(),
+ 'root' => base_path(),
],
'ftp' => [
diff --git a/config/setting-defaults.php b/config/setting-defaults.php
index 5482c1331..c681bb7f5 100644
--- a/config/setting-defaults.php
+++ b/config/setting-defaults.php
@@ -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,
];
\ No newline at end of file
diff --git a/database/migrations/2016_09_29_101449_remove_hidden_roles.php b/database/migrations/2016_09_29_101449_remove_hidden_roles.php
new file mode 100644
index 000000000..f666cad2c
--- /dev/null
+++ b/database/migrations/2016_09_29_101449_remove_hidden_roles.php
@@ -0,0 +1,66 @@
+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]);
+ }
+}
diff --git a/database/migrations/2016_10_09_142037_create_attachments_table.php b/database/migrations/2016_10_09_142037_create_attachments_table.php
new file mode 100644
index 000000000..627c237c4
--- /dev/null
+++ b/database/migrations/2016_10_09_142037_create_attachments_table.php
@@ -0,0 +1,71 @@
+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();
+ }
+ }
+}
diff --git a/gulpfile.js b/gulpfile.js
index 7deefc71a..9d789d9b4 100644
--- a/gulpfile.js
+++ b/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');
});
diff --git a/package.json b/package.json
index fde090beb..30f288d45 100644
--- a/package.json
+++ b/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"
diff --git a/phpunit.xml b/phpunit.xml
index a2b26d413..72e06a3fc 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -30,6 +30,7 @@
Upload some files or attach some link to display on your page. These are visible in the page sidebar. Changes here are saved instantly.
+ ++ |
+
+
+ Click delete again to confirm you want to delete this attachment.
+
+ + Cancel + |
+ + | + | + |
+ No files have been uploaded. +
+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.
+value) colspan="2" @endif>{{ $tag->name }} | + @if($tag->value){{$tag->value}} | @endif +
This user represents any guest users that visit your instance. It cannot be used for logins but is assigned automatically.
+@endif + +