Dan Brown

Merge branch 'master' into release

Showing 127 changed files with 2221 additions and 559 deletions
...@@ -10,4 +10,6 @@ Homestead.yaml ...@@ -10,4 +10,6 @@ Homestead.yaml
10 /public/bower 10 /public/bower
11 /storage/images 11 /storage/images
12 _ide_helper.php 12 _ide_helper.php
13 -/storage/debugbar
...\ No newline at end of file ...\ No newline at end of file
13 +/storage/debugbar
14 +.phpstorm.meta.php
15 +yarn.lock
......
1 +<?php namespace BookStack;
2 +
3 +
4 +class Attachment extends Ownable
5 +{
6 + protected $fillable = ['name', 'order'];
7 +
8 + /**
9 + * Get the downloadable file name for this upload.
10 + * @return mixed|string
11 + */
12 + public function getFileName()
13 + {
14 + if (str_contains($this->name, '.')) return $this->name;
15 + return $this->name . '.' . $this->extension;
16 + }
17 +
18 + /**
19 + * Get the page this file was uploaded to.
20 + * @return Page
21 + */
22 + public function page()
23 + {
24 + return $this->belongsTo(Page::class, 'uploaded_to');
25 + }
26 +
27 + /**
28 + * Get the url of this file.
29 + * @return string
30 + */
31 + public function getUrl()
32 + {
33 + return baseUrl('/attachments/' . $this->id);
34 + }
35 +
36 +}
...@@ -13,9 +13,9 @@ class Book extends Entity ...@@ -13,9 +13,9 @@ class Book extends Entity
13 public function getUrl($path = false) 13 public function getUrl($path = false)
14 { 14 {
15 if ($path !== false) { 15 if ($path !== false) {
16 - return baseUrl('/books/' . $this->slug . '/' . trim($path, '/')); 16 + return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
17 } 17 }
18 - return baseUrl('/books/' . $this->slug); 18 + return baseUrl('/books/' . urlencode($this->slug));
19 } 19 }
20 20
21 /* 21 /*
......
...@@ -32,9 +32,9 @@ class Chapter extends Entity ...@@ -32,9 +32,9 @@ class Chapter extends Entity
32 { 32 {
33 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; 33 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
34 if ($path !== false) { 34 if ($path !== false) {
35 - return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/')); 35 + return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
36 } 36 }
37 - return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug); 37 + return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
38 } 38 }
39 39
40 /** 40 /**
......
1 -<?php namespace BookStack;
2 -
3 -class EmailConfirmation extends Model
4 -{
5 - protected $fillable = ['user_id', 'token'];
6 -
7 - /**
8 - * Get the user that this confirmation is attached to.
9 - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
10 - */
11 - public function user()
12 - {
13 - return $this->belongsTo(User::class);
14 - }
15 -
16 -}
...@@ -162,18 +162,21 @@ class Entity extends Ownable ...@@ -162,18 +162,21 @@ class Entity extends Ownable
162 $exactTerms = []; 162 $exactTerms = [];
163 $fuzzyTerms = []; 163 $fuzzyTerms = [];
164 $search = static::newQuery(); 164 $search = static::newQuery();
165 +
165 foreach ($terms as $key => $term) { 166 foreach ($terms as $key => $term) {
166 - $safeTerm = htmlentities($term, ENT_QUOTES); 167 + $term = htmlentities($term, ENT_QUOTES);
167 - $safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm); 168 + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
168 - if (preg_match('/&quot;.*?&quot;/', $safeTerm) || is_numeric($safeTerm)) { 169 + if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
169 - $safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term); 170 + $term = str_replace('&quot;', '', $term);
170 - $exactTerms[] = '%' . $safeTerm . '%'; 171 + $exactTerms[] = '%' . $term . '%';
171 } else { 172 } else {
172 - $safeTerm = '' . $safeTerm . '*'; 173 + $term = '' . $term . '*';
173 - if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm; 174 + if ($term !== '*') $fuzzyTerms[] = $term;
174 } 175 }
175 } 176 }
176 - $isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0; 177 +
178 + $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
179 +
177 180
178 // Perform fulltext search if relevant terms exist. 181 // Perform fulltext search if relevant terms exist.
179 if ($isFuzzy) { 182 if ($isFuzzy) {
...@@ -193,6 +196,7 @@ class Entity extends Ownable ...@@ -193,6 +196,7 @@ class Entity extends Ownable
193 } 196 }
194 }); 197 });
195 } 198 }
199 +
196 $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at'; 200 $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
197 201
198 // Add additional where terms 202 // Add additional where terms
......
1 -<?php
2 -
3 -namespace BookStack\Events;
4 -
5 -abstract class Event
6 -{
7 - //
8 -}
1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +class FileUploadException extends PrettyException {}
...\ No newline at end of file ...\ No newline at end of file
...@@ -87,4 +87,20 @@ class Handler extends ExceptionHandler ...@@ -87,4 +87,20 @@ class Handler extends ExceptionHandler
87 } while ($e = $e->getPrevious()); 87 } while ($e = $e->getPrevious());
88 return $message; 88 return $message;
89 } 89 }
90 +
91 + /**
92 + * Convert an authentication exception into an unauthenticated response.
93 + *
94 + * @param \Illuminate\Http\Request $request
95 + * @param \Illuminate\Auth\AuthenticationException $exception
96 + * @return \Illuminate\Http\Response
97 + */
98 + protected function unauthenticated($request, AuthenticationException $exception)
99 + {
100 + if ($request->expectsJson()) {
101 + return response()->json(['error' => 'Unauthenticated.'], 401);
102 + }
103 +
104 + return redirect()->guest('login');
105 + }
90 } 106 }
......
1 +<?php namespace BookStack\Http\Controllers;
2 +
3 +use BookStack\Exceptions\FileUploadException;
4 +use BookStack\Attachment;
5 +use BookStack\Repos\PageRepo;
6 +use BookStack\Services\AttachmentService;
7 +use Illuminate\Http\Request;
8 +
9 +class AttachmentController extends Controller
10 +{
11 + protected $attachmentService;
12 + protected $attachment;
13 + protected $pageRepo;
14 +
15 + /**
16 + * AttachmentController constructor.
17 + * @param AttachmentService $attachmentService
18 + * @param Attachment $attachment
19 + * @param PageRepo $pageRepo
20 + */
21 + public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
22 + {
23 + $this->attachmentService = $attachmentService;
24 + $this->attachment = $attachment;
25 + $this->pageRepo = $pageRepo;
26 + parent::__construct();
27 + }
28 +
29 +
30 + /**
31 + * Endpoint at which attachments are uploaded to.
32 + * @param Request $request
33 + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
34 + */
35 + public function upload(Request $request)
36 + {
37 + $this->validate($request, [
38 + 'uploaded_to' => 'required|integer|exists:pages,id',
39 + 'file' => 'required|file'
40 + ]);
41 +
42 + $pageId = $request->get('uploaded_to');
43 + $page = $this->pageRepo->getById($pageId, true);
44 +
45 + $this->checkPermission('attachment-create-all');
46 + $this->checkOwnablePermission('page-update', $page);
47 +
48 + $uploadedFile = $request->file('file');
49 +
50 + try {
51 + $attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
52 + } catch (FileUploadException $e) {
53 + return response($e->getMessage(), 500);
54 + }
55 +
56 + return response()->json($attachment);
57 + }
58 +
59 + /**
60 + * Update an uploaded attachment.
61 + * @param int $attachmentId
62 + * @param Request $request
63 + * @return mixed
64 + */
65 + public function uploadUpdate($attachmentId, Request $request)
66 + {
67 + $this->validate($request, [
68 + 'uploaded_to' => 'required|integer|exists:pages,id',
69 + 'file' => 'required|file'
70 + ]);
71 +
72 + $pageId = $request->get('uploaded_to');
73 + $page = $this->pageRepo->getById($pageId, true);
74 + $attachment = $this->attachment->findOrFail($attachmentId);
75 +
76 + $this->checkOwnablePermission('page-update', $page);
77 + $this->checkOwnablePermission('attachment-create', $attachment);
78 +
79 + if (intval($pageId) !== intval($attachment->uploaded_to)) {
80 + return $this->jsonError('Page mismatch during attached file update');
81 + }
82 +
83 + $uploadedFile = $request->file('file');
84 +
85 + try {
86 + $attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
87 + } catch (FileUploadException $e) {
88 + return response($e->getMessage(), 500);
89 + }
90 +
91 + return response()->json($attachment);
92 + }
93 +
94 + /**
95 + * Update the details of an existing file.
96 + * @param $attachmentId
97 + * @param Request $request
98 + * @return Attachment|mixed
99 + */
100 + public function update($attachmentId, Request $request)
101 + {
102 + $this->validate($request, [
103 + 'uploaded_to' => 'required|integer|exists:pages,id',
104 + 'name' => 'required|string|min:1|max:255',
105 + 'link' => 'url|min:1|max:255'
106 + ]);
107 +
108 + $pageId = $request->get('uploaded_to');
109 + $page = $this->pageRepo->getById($pageId, true);
110 + $attachment = $this->attachment->findOrFail($attachmentId);
111 +
112 + $this->checkOwnablePermission('page-update', $page);
113 + $this->checkOwnablePermission('attachment-create', $attachment);
114 +
115 + if (intval($pageId) !== intval($attachment->uploaded_to)) {
116 + return $this->jsonError('Page mismatch during attachment update');
117 + }
118 +
119 + $attachment = $this->attachmentService->updateFile($attachment, $request->all());
120 + return $attachment;
121 + }
122 +
123 + /**
124 + * Attach a link to a page.
125 + * @param Request $request
126 + * @return mixed
127 + */
128 + public function attachLink(Request $request)
129 + {
130 + $this->validate($request, [
131 + 'uploaded_to' => 'required|integer|exists:pages,id',
132 + 'name' => 'required|string|min:1|max:255',
133 + 'link' => 'required|url|min:1|max:255'
134 + ]);
135 +
136 + $pageId = $request->get('uploaded_to');
137 + $page = $this->pageRepo->getById($pageId, true);
138 +
139 + $this->checkPermission('attachment-create-all');
140 + $this->checkOwnablePermission('page-update', $page);
141 +
142 + $attachmentName = $request->get('name');
143 + $link = $request->get('link');
144 + $attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
145 +
146 + return response()->json($attachment);
147 + }
148 +
149 + /**
150 + * Get the attachments for a specific page.
151 + * @param $pageId
152 + * @return mixed
153 + */
154 + public function listForPage($pageId)
155 + {
156 + $page = $this->pageRepo->getById($pageId, true);
157 + $this->checkOwnablePermission('page-view', $page);
158 + return response()->json($page->attachments);
159 + }
160 +
161 + /**
162 + * Update the attachment sorting.
163 + * @param $pageId
164 + * @param Request $request
165 + * @return mixed
166 + */
167 + public function sortForPage($pageId, Request $request)
168 + {
169 + $this->validate($request, [
170 + 'files' => 'required|array',
171 + 'files.*.id' => 'required|integer',
172 + ]);
173 + $page = $this->pageRepo->getById($pageId);
174 + $this->checkOwnablePermission('page-update', $page);
175 +
176 + $attachments = $request->get('files');
177 + $this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
178 + return response()->json(['message' => 'Attachment order updated']);
179 + }
180 +
181 + /**
182 + * Get an attachment from storage.
183 + * @param $attachmentId
184 + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
185 + */
186 + public function get($attachmentId)
187 + {
188 + $attachment = $this->attachment->findOrFail($attachmentId);
189 + $page = $this->pageRepo->getById($attachment->uploaded_to);
190 + $this->checkOwnablePermission('page-view', $page);
191 +
192 + if ($attachment->external) {
193 + return redirect($attachment->path);
194 + }
195 +
196 + $attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
197 + return response($attachmentContents, 200, [
198 + 'Content-Type' => 'application/octet-stream',
199 + 'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
200 + ]);
201 + }
202 +
203 + /**
204 + * Delete a specific attachment in the system.
205 + * @param $attachmentId
206 + * @return mixed
207 + */
208 + public function delete($attachmentId)
209 + {
210 + $attachment = $this->attachment->findOrFail($attachmentId);
211 + $this->checkOwnablePermission('attachment-delete', $attachment);
212 + $this->attachmentService->deleteFile($attachment);
213 + return response()->json(['message' => 'Attachment deleted']);
214 + }
215 +}
1 +<?php
2 +
3 +namespace BookStack\Http\Controllers\Auth;
4 +
5 +use BookStack\Http\Controllers\Controller;
6 +use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
7 +use Illuminate\Http\Request;
8 +use Password;
9 +
10 +class ForgotPasswordController extends Controller
11 +{
12 + /*
13 + |--------------------------------------------------------------------------
14 + | Password Reset Controller
15 + |--------------------------------------------------------------------------
16 + |
17 + | This controller is responsible for handling password reset emails and
18 + | includes a trait which assists in sending these notifications from
19 + | your application to your users. Feel free to explore this trait.
20 + |
21 + */
22 +
23 + use SendsPasswordResetEmails;
24 +
25 + /**
26 + * Create a new controller instance.
27 + *
28 + * @return void
29 + */
30 + public function __construct()
31 + {
32 + $this->middleware('guest');
33 + parent::__construct();
34 + }
35 +
36 +
37 + /**
38 + * Send a reset link to the given user.
39 + *
40 + * @param \Illuminate\Http\Request $request
41 + * @return \Illuminate\Http\RedirectResponse
42 + */
43 + public function sendResetLinkEmail(Request $request)
44 + {
45 + $this->validate($request, ['email' => 'required|email']);
46 +
47 + // We will send the password reset link to this user. Once we have attempted
48 + // to send the link, we will examine the response then see the message we
49 + // need to show to the user. Finally, we'll send out a proper response.
50 + $response = $this->broker()->sendResetLink(
51 + $request->only('email')
52 + );
53 +
54 + if ($response === Password::RESET_LINK_SENT) {
55 + $message = 'A password reset link has been sent to ' . $request->get('email') . '.';
56 + session()->flash('success', $message);
57 + return back()->with('status', trans($response));
58 + }
59 +
60 + // If an error was returned by the password broker, we will get this message
61 + // translated so we can notify a user of the problem. We'll redirect back
62 + // to where the users came from so they can attempt this process again.
63 + return back()->withErrors(
64 + ['email' => trans($response)]
65 + );
66 + }
67 +
68 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php
2 +
3 +namespace BookStack\Http\Controllers\Auth;
4 +
5 +use BookStack\Http\Controllers\Controller;
6 +use BookStack\Repos\UserRepo;
7 +use BookStack\Services\SocialAuthService;
8 +use Illuminate\Contracts\Auth\Authenticatable;
9 +use Illuminate\Foundation\Auth\AuthenticatesUsers;
10 +use Illuminate\Http\Request;
11 +
12 +class LoginController extends Controller
13 +{
14 + /*
15 + |--------------------------------------------------------------------------
16 + | Login Controller
17 + |--------------------------------------------------------------------------
18 + |
19 + | This controller handles authenticating users for the application and
20 + | redirecting them to your home screen. The controller uses a trait
21 + | to conveniently provide its functionality to your applications.
22 + |
23 + */
24 +
25 + use AuthenticatesUsers;
26 +
27 + /**
28 + * Where to redirect users after login.
29 + *
30 + * @var string
31 + */
32 + protected $redirectTo = '/';
33 +
34 + protected $redirectPath = '/';
35 + protected $redirectAfterLogout = '/login';
36 +
37 + protected $socialAuthService;
38 + protected $userRepo;
39 +
40 + /**
41 + * Create a new controller instance.
42 + *
43 + * @param SocialAuthService $socialAuthService
44 + * @param UserRepo $userRepo
45 + */
46 + public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
47 + {
48 + $this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
49 + $this->socialAuthService = $socialAuthService;
50 + $this->userRepo = $userRepo;
51 + $this->redirectPath = baseUrl('/');
52 + $this->redirectAfterLogout = baseUrl('/login');
53 + parent::__construct();
54 + }
55 +
56 + public function username()
57 + {
58 + return config('auth.method') === 'standard' ? 'email' : 'username';
59 + }
60 +
61 + /**
62 + * Overrides the action when a user is authenticated.
63 + * If the user authenticated but does not exist in the user table we create them.
64 + * @param Request $request
65 + * @param Authenticatable $user
66 + * @return \Illuminate\Http\RedirectResponse
67 + * @throws AuthException
68 + */
69 + protected function authenticated(Request $request, Authenticatable $user)
70 + {
71 + // Explicitly log them out for now if they do no exist.
72 + if (!$user->exists) auth()->logout($user);
73 +
74 + if (!$user->exists && $user->email === null && !$request->has('email')) {
75 + $request->flash();
76 + session()->flash('request-email', true);
77 + return redirect('/login');
78 + }
79 +
80 + if (!$user->exists && $user->email === null && $request->has('email')) {
81 + $user->email = $request->get('email');
82 + }
83 +
84 + if (!$user->exists) {
85 +
86 + // Check for users with same email already
87 + $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
88 + if ($alreadyUser) {
89 + throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
90 + }
91 +
92 + $user->save();
93 + $this->userRepo->attachDefaultRole($user);
94 + auth()->login($user);
95 + }
96 +
97 + $path = session()->pull('url.intended', '/');
98 + $path = baseUrl($path, true);
99 + return redirect($path);
100 + }
101 +
102 + /**
103 + * Show the application login form.
104 + * @return \Illuminate\Http\Response
105 + */
106 + public function getLogin()
107 + {
108 + $socialDrivers = $this->socialAuthService->getActiveDrivers();
109 + $authMethod = config('auth.method');
110 + return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
111 + }
112 +
113 + /**
114 + * Redirect to the relevant social site.
115 + * @param $socialDriver
116 + * @return \Symfony\Component\HttpFoundation\RedirectResponse
117 + */
118 + public function getSocialLogin($socialDriver)
119 + {
120 + session()->put('social-callback', 'login');
121 + return $this->socialAuthService->startLogIn($socialDriver);
122 + }
123 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -4,10 +4,8 @@ namespace BookStack\Http\Controllers\Auth; ...@@ -4,10 +4,8 @@ namespace BookStack\Http\Controllers\Auth;
4 4
5 use BookStack\Http\Controllers\Controller; 5 use BookStack\Http\Controllers\Controller;
6 use Illuminate\Foundation\Auth\ResetsPasswords; 6 use Illuminate\Foundation\Auth\ResetsPasswords;
7 -use Illuminate\Http\Request;
8 -use Password;
9 7
10 -class PasswordController extends Controller 8 +class ResetPasswordController extends Controller
11 { 9 {
12 /* 10 /*
13 |-------------------------------------------------------------------------- 11 |--------------------------------------------------------------------------
...@@ -25,52 +23,27 @@ class PasswordController extends Controller ...@@ -25,52 +23,27 @@ class PasswordController extends Controller
25 protected $redirectTo = '/'; 23 protected $redirectTo = '/';
26 24
27 /** 25 /**
28 - * Create a new password controller instance. 26 + * Create a new controller instance.
27 + *
28 + * @return void
29 */ 29 */
30 public function __construct() 30 public function __construct()
31 { 31 {
32 $this->middleware('guest'); 32 $this->middleware('guest');
33 - } 33 + parent::__construct();
34 -
35 -
36 - /**
37 - * Send a reset link to the given user.
38 - *
39 - * @param \Illuminate\Http\Request $request
40 - * @return \Illuminate\Http\Response
41 - */
42 - public function sendResetLinkEmail(Request $request)
43 - {
44 - $this->validate($request, ['email' => 'required|email']);
45 -
46 - $broker = $this->getBroker();
47 -
48 - $response = Password::broker($broker)->sendResetLink(
49 - $request->only('email'), $this->resetEmailBuilder()
50 - );
51 -
52 - switch ($response) {
53 - case Password::RESET_LINK_SENT:
54 - $message = 'A password reset link has been sent to ' . $request->get('email') . '.';
55 - session()->flash('success', $message);
56 - return $this->getSendResetLinkEmailSuccessResponse($response);
57 -
58 - case Password::INVALID_USER:
59 - default:
60 - return $this->getSendResetLinkEmailFailureResponse($response);
61 - }
62 } 34 }
63 35
64 /** 36 /**
65 - * Get the response for after a successful password reset. 37 + * Get the response for a successful password reset.
66 * 38 *
67 * @param string $response 39 * @param string $response
68 - * @return \Symfony\Component\HttpFoundation\Response 40 + * @return \Illuminate\Http\Response
69 */ 41 */
70 - protected function getResetSuccessResponse($response) 42 + protected function sendResetResponse($response)
71 { 43 {
72 $message = 'Your password has been successfully reset.'; 44 $message = 'Your password has been successfully reset.';
73 session()->flash('success', $message); 45 session()->flash('success', $message);
74 - return redirect($this->redirectPath())->with('status', trans($response)); 46 + return redirect($this->redirectPath())
47 + ->with('status', trans($response));
75 } 48 }
76 -} 49 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -115,9 +115,11 @@ class ChapterController extends Controller ...@@ -115,9 +115,11 @@ class ChapterController extends Controller
115 $book = $this->bookRepo->getBySlug($bookSlug); 115 $book = $this->bookRepo->getBySlug($bookSlug);
116 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 116 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
117 $this->checkOwnablePermission('chapter-update', $chapter); 117 $this->checkOwnablePermission('chapter-update', $chapter);
118 + if ($chapter->name !== $request->get('name')) {
119 + $chapter->slug = $this->chapterRepo->findSuitableSlug($request->get('name'), $book->id, $chapter->id);
120 + }
118 $chapter->fill($request->all()); 121 $chapter->fill($request->all());
119 - $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id); 122 + $chapter->updated_by = user()->id;
120 - $chapter->updated_by = auth()->user()->id;
121 $chapter->save(); 123 $chapter->save();
122 Activity::add($chapter, 'chapter_update', $book->id); 124 Activity::add($chapter, 'chapter_update', $book->id);
123 return redirect($chapter->getUrl()); 125 return redirect($chapter->getUrl());
......
...@@ -3,13 +3,11 @@ ...@@ -3,13 +3,11 @@
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 use BookStack\Ownable; 5 use BookStack\Ownable;
6 -use HttpRequestException;
7 use Illuminate\Foundation\Bus\DispatchesJobs; 6 use Illuminate\Foundation\Bus\DispatchesJobs;
8 use Illuminate\Http\Exception\HttpResponseException; 7 use Illuminate\Http\Exception\HttpResponseException;
8 +use Illuminate\Http\Request;
9 use Illuminate\Routing\Controller as BaseController; 9 use Illuminate\Routing\Controller as BaseController;
10 use Illuminate\Foundation\Validation\ValidatesRequests; 10 use Illuminate\Foundation\Validation\ValidatesRequests;
11 -use Illuminate\Support\Facades\Auth;
12 -use Illuminate\Support\Facades\Session;
13 use BookStack\User; 11 use BookStack\User;
14 12
15 abstract class Controller extends BaseController 13 abstract class Controller extends BaseController
...@@ -30,17 +28,21 @@ abstract class Controller extends BaseController ...@@ -30,17 +28,21 @@ abstract class Controller extends BaseController
30 */ 28 */
31 public function __construct() 29 public function __construct()
32 { 30 {
33 - // Get a user instance for the current user 31 + $this->middleware(function ($request, $next) {
34 - $user = auth()->user();
35 - if (!$user) $user = User::getDefault();
36 32
37 - // Share variables with views 33 + // Get a user instance for the current user
38 - view()->share('signedIn', auth()->check()); 34 + $user = user();
39 - view()->share('currentUser', $user);
40 35
41 - // Share variables with controllers 36 + // Share variables with controllers
42 - $this->currentUser = $user; 37 + $this->currentUser = $user;
43 - $this->signedIn = auth()->check(); 38 + $this->signedIn = auth()->check();
39 +
40 + // Share variables with views
41 + view()->share('signedIn', $this->signedIn);
42 + view()->share('currentUser', $user);
43 +
44 + return $next($request);
45 + });
44 } 46 }
45 47
46 /** 48 /**
...@@ -67,8 +69,13 @@ abstract class Controller extends BaseController ...@@ -67,8 +69,13 @@ abstract class Controller extends BaseController
67 */ 69 */
68 protected function showPermissionError() 70 protected function showPermissionError()
69 { 71 {
70 - Session::flash('error', trans('errors.permission')); 72 + if (request()->wantsJson()) {
71 - $response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/'); 73 + $response = response()->json(['error' => trans('errors.permissionJson')], 403);
74 + } else {
75 + $response = redirect('/');
76 + session()->flash('error', trans('errors.permission'));
77 + }
78 +
72 throw new HttpResponseException($response); 79 throw new HttpResponseException($response);
73 } 80 }
74 81
...@@ -79,7 +86,7 @@ abstract class Controller extends BaseController ...@@ -79,7 +86,7 @@ abstract class Controller extends BaseController
79 */ 86 */
80 protected function checkPermission($permissionName) 87 protected function checkPermission($permissionName)
81 { 88 {
82 - if (!$this->currentUser || !$this->currentUser->can($permissionName)) { 89 + if (!user() || !user()->can($permissionName)) {
83 $this->showPermissionError(); 90 $this->showPermissionError();
84 } 91 }
85 return true; 92 return true;
...@@ -121,4 +128,22 @@ abstract class Controller extends BaseController ...@@ -121,4 +128,22 @@ abstract class Controller extends BaseController
121 return response()->json(['message' => $messageText], $statusCode); 128 return response()->json(['message' => $messageText], $statusCode);
122 } 129 }
123 130
131 + /**
132 + * Create the response for when a request fails validation.
133 + *
134 + * @param \Illuminate\Http\Request $request
135 + * @param array $errors
136 + * @return \Symfony\Component\HttpFoundation\Response
137 + */
138 + protected function buildFailedValidationResponse(Request $request, array $errors)
139 + {
140 + if ($request->expectsJson()) {
141 + return response()->json(['validation' => $errors], 422);
142 + }
143 +
144 + return redirect()->to($this->getRedirectUrl())
145 + ->withInput($request->input())
146 + ->withErrors($errors, $this->errorBag());
147 + }
148 +
124 } 149 }
......
...@@ -17,10 +17,7 @@ class SettingController extends Controller ...@@ -17,10 +17,7 @@ class SettingController extends Controller
17 $this->setPageTitle('Settings'); 17 $this->setPageTitle('Settings');
18 18
19 // Get application version 19 // Get application version
20 - $version = false; 20 + $version = trim(file_get_contents(base_path('version')));
21 - if (function_exists('exec')) {
22 - $version = exec('git describe --always --tags ');
23 - }
24 21
25 return view('settings/index', ['version' => $version]); 22 return view('settings/index', ['version' => $version]);
26 } 23 }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 use BookStack\Activity; 5 use BookStack\Activity;
6 +use Exception;
6 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
7 8
8 use Illuminate\Http\Response; 9 use Illuminate\Http\Response;
...@@ -56,7 +57,7 @@ class UserController extends Controller ...@@ -56,7 +57,7 @@ class UserController extends Controller
56 { 57 {
57 $this->checkPermission('users-manage'); 58 $this->checkPermission('users-manage');
58 $authMethod = config('auth.method'); 59 $authMethod = config('auth.method');
59 - $roles = $this->userRepo->getAssignableRoles(); 60 + $roles = $this->userRepo->getAllRoles();
60 return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]); 61 return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
61 } 62 }
62 63
...@@ -100,9 +101,14 @@ class UserController extends Controller ...@@ -100,9 +101,14 @@ class UserController extends Controller
100 101
101 // Get avatar from gravatar and save 102 // Get avatar from gravatar and save
102 if (!config('services.disable_services')) { 103 if (!config('services.disable_services')) {
103 - $avatar = \Images::saveUserGravatar($user); 104 + try {
104 - $user->avatar()->associate($avatar); 105 + $avatar = \Images::saveUserGravatar($user);
105 - $user->save(); 106 + $user->avatar()->associate($avatar);
107 + $user->save();
108 + } catch (Exception $e) {
109 + \Log::error('Failed to save user gravatar image');
110 + }
111 +
106 } 112 }
107 113
108 return redirect('/settings/users'); 114 return redirect('/settings/users');
...@@ -120,12 +126,13 @@ class UserController extends Controller ...@@ -120,12 +126,13 @@ class UserController extends Controller
120 return $this->currentUser->id == $id; 126 return $this->currentUser->id == $id;
121 }); 127 });
122 128
123 - $authMethod = config('auth.method');
124 -
125 $user = $this->user->findOrFail($id); 129 $user = $this->user->findOrFail($id);
130 +
131 + $authMethod = ($user->system_name) ? 'system' : config('auth.method');
132 +
126 $activeSocialDrivers = $socialAuthService->getActiveDrivers(); 133 $activeSocialDrivers = $socialAuthService->getActiveDrivers();
127 $this->setPageTitle('User Profile'); 134 $this->setPageTitle('User Profile');
128 - $roles = $this->userRepo->getAssignableRoles(); 135 + $roles = $this->userRepo->getAllRoles();
129 return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]); 136 return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
130 } 137 }
131 138
...@@ -180,7 +187,7 @@ class UserController extends Controller ...@@ -180,7 +187,7 @@ class UserController extends Controller
180 187
181 /** 188 /**
182 * Show the user delete page. 189 * Show the user delete page.
183 - * @param $id 190 + * @param int $id
184 * @return \Illuminate\View\View 191 * @return \Illuminate\View\View
185 */ 192 */
186 public function delete($id) 193 public function delete($id)
...@@ -213,6 +220,11 @@ class UserController extends Controller ...@@ -213,6 +220,11 @@ class UserController extends Controller
213 return redirect($user->getEditUrl()); 220 return redirect($user->getEditUrl());
214 } 221 }
215 222
223 + if ($user->system_name === 'public') {
224 + session()->flash('error', 'You cannot delete the guest user');
225 + return redirect($user->getEditUrl());
226 + }
227 +
216 $this->userRepo->destroy($user); 228 $this->userRepo->destroy($user);
217 session()->flash('success', 'User successfully removed'); 229 session()->flash('success', 'User successfully removed');
218 230
......
...@@ -9,15 +9,32 @@ class Kernel extends HttpKernel ...@@ -9,15 +9,32 @@ class Kernel extends HttpKernel
9 /** 9 /**
10 * The application's global HTTP middleware stack. 10 * The application's global HTTP middleware stack.
11 * 11 *
12 + * These middleware are run during every request to your application.
13 + *
12 * @var array 14 * @var array
13 */ 15 */
14 protected $middleware = [ 16 protected $middleware = [
15 \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, 17 \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
16 - \BookStack\Http\Middleware\EncryptCookies::class, 18 + ];
17 - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 19 +
18 - \Illuminate\Session\Middleware\StartSession::class, 20 + /**
19 - \Illuminate\View\Middleware\ShareErrorsFromSession::class, 21 + * The application's route middleware groups.
20 - \BookStack\Http\Middleware\VerifyCsrfToken::class, 22 + *
23 + * @var array
24 + */
25 + protected $middlewareGroups = [
26 + 'web' => [
27 + \BookStack\Http\Middleware\EncryptCookies::class,
28 + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
29 + \Illuminate\Session\Middleware\StartSession::class,
30 + \Illuminate\View\Middleware\ShareErrorsFromSession::class,
31 + \BookStack\Http\Middleware\VerifyCsrfToken::class,
32 + \Illuminate\Routing\Middleware\SubstituteBindings::class,
33 + ],
34 + 'api' => [
35 + 'throttle:60,1',
36 + 'bindings',
37 + ],
21 ]; 38 ];
22 39
23 /** 40 /**
...@@ -26,6 +43,7 @@ class Kernel extends HttpKernel ...@@ -26,6 +43,7 @@ class Kernel extends HttpKernel
26 * @var array 43 * @var array
27 */ 44 */
28 protected $routeMiddleware = [ 45 protected $routeMiddleware = [
46 + 'can' => \Illuminate\Auth\Middleware\Authorize::class,
29 'auth' => \BookStack\Http\Middleware\Authenticate::class, 47 'auth' => \BookStack\Http\Middleware\Authenticate::class,
30 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 48 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
31 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class, 49 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
......
...@@ -33,7 +33,7 @@ class Authenticate ...@@ -33,7 +33,7 @@ class Authenticate
33 public function handle($request, Closure $next) 33 public function handle($request, Closure $next)
34 { 34 {
35 if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) { 35 if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
36 - return redirect()->guest(baseUrl('/register/confirm/awaiting')); 36 + return redirect(baseUrl('/register/confirm/awaiting'));
37 } 37 }
38 38
39 if ($this->auth->guest() && !setting('app-public')) { 39 if ($this->auth->guest() && !setting('app-public')) {
......
...@@ -34,7 +34,8 @@ class RedirectIfAuthenticated ...@@ -34,7 +34,8 @@ class RedirectIfAuthenticated
34 */ 34 */
35 public function handle($request, Closure $next) 35 public function handle($request, Closure $next)
36 { 36 {
37 - if ($this->auth->check()) { 37 + $requireConfirmation = setting('registration-confirmation');
38 + if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
38 return redirect('/'); 39 return redirect('/');
39 } 40 }
40 41
......
1 -<?php
2 -
3 -namespace BookStack\Jobs;
4 -
5 -use Illuminate\Bus\Queueable;
6 -
7 -abstract class Job
8 -{
9 - /*
10 - |--------------------------------------------------------------------------
11 - | Queueable Jobs
12 - |--------------------------------------------------------------------------
13 - |
14 - | This job base class provides a central location to place any logic that
15 - | is shared across all of your jobs. The trait included with the class
16 - | provides access to the "queueOn" and "delay" queue helper methods.
17 - |
18 - */
19 -
20 - use Queueable;
21 -}
1 +<?php
2 +
3 +namespace BookStack\Notifications;
4 +
5 +use Illuminate\Notifications\Notification;
6 +use Illuminate\Notifications\Messages\MailMessage;
7 +
8 +class ConfirmEmail extends Notification
9 +{
10 +
11 + public $token;
12 +
13 + /**
14 + * Create a new notification instance.
15 + * @param string $token
16 + */
17 + public function __construct($token)
18 + {
19 + $this->token = $token;
20 + }
21 +
22 + /**
23 + * Get the notification's delivery channels.
24 + *
25 + * @param mixed $notifiable
26 + * @return array
27 + */
28 + public function via($notifiable)
29 + {
30 + return ['mail'];
31 + }
32 +
33 + /**
34 + * Get the mail representation of the notification.
35 + *
36 + * @param mixed $notifiable
37 + * @return \Illuminate\Notifications\Messages\MailMessage
38 + */
39 + public function toMail($notifiable)
40 + {
41 + $appName = ['appName' => setting('app-name')];
42 + return (new MailMessage)
43 + ->subject(trans('auth.email_confirm_subject', $appName))
44 + ->greeting(trans('auth.email_confirm_greeting', $appName))
45 + ->line(trans('auth.email_confirm_text'))
46 + ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
47 + }
48 +
49 +}
1 +<?php
2 +
3 +namespace BookStack\Notifications;
4 +
5 +use Illuminate\Notifications\Notification;
6 +use Illuminate\Notifications\Messages\MailMessage;
7 +
8 +class ResetPassword extends Notification
9 +{
10 + /**
11 + * The password reset token.
12 + *
13 + * @var string
14 + */
15 + public $token;
16 +
17 + /**
18 + * Create a notification instance.
19 + *
20 + * @param string $token
21 + */
22 + public function __construct($token)
23 + {
24 + $this->token = $token;
25 + }
26 +
27 + /**
28 + * Get the notification's channels.
29 + *
30 + * @param mixed $notifiable
31 + * @return array|string
32 + */
33 + public function via($notifiable)
34 + {
35 + return ['mail'];
36 + }
37 +
38 + /**
39 + * Build the mail representation of the notification.
40 + *
41 + * @return \Illuminate\Notifications\Messages\MailMessage
42 + */
43 + public function toMail()
44 + {
45 + return (new MailMessage)
46 + ->line('You are receiving this email because we received a password reset request for your account.')
47 + ->action('Reset Password', baseUrl('password/reset/' . $this->token))
48 + ->line('If you did not request a password reset, no further action is required.');
49 + }
50 +}
...@@ -55,6 +55,15 @@ class Page extends Entity ...@@ -55,6 +55,15 @@ class Page extends Entity
55 } 55 }
56 56
57 /** 57 /**
58 + * Get the attachments assigned to this page.
59 + * @return \Illuminate\Database\Eloquent\Relations\HasMany
60 + */
61 + public function attachments()
62 + {
63 + return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
64 + }
65 +
66 + /**
58 * Get the url for this page. 67 * Get the url for this page.
59 * @param string|bool $path 68 * @param string|bool $path
60 * @return string 69 * @return string
...@@ -63,13 +72,13 @@ class Page extends Entity ...@@ -63,13 +72,13 @@ class Page extends Entity
63 { 72 {
64 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; 73 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
65 $midText = $this->draft ? '/draft/' : '/page/'; 74 $midText = $this->draft ? '/draft/' : '/page/';
66 - $idComponent = $this->draft ? $this->id : $this->slug; 75 + $idComponent = $this->draft ? $this->id : urlencode($this->slug);
67 76
68 if ($path !== false) { 77 if ($path !== false) {
69 - return baseUrl('/books/' . $bookSlug . $midText . $idComponent . '/' . trim($path, '/')); 78 + return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
70 } 79 }
71 80
72 - return baseUrl('/books/' . $bookSlug . $midText . $idComponent); 81 + return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
73 } 82 }
74 83
75 /** 84 /**
......
...@@ -25,11 +25,26 @@ class PageRevision extends Model ...@@ -25,11 +25,26 @@ class PageRevision extends Model
25 25
26 /** 26 /**
27 * Get the url for this revision. 27 * Get the url for this revision.
28 + * @param null|string $path
28 * @return string 29 * @return string
29 */ 30 */
30 - public function getUrl() 31 + public function getUrl($path = null)
31 { 32 {
32 - return $this->page->getUrl() . '/revisions/' . $this->id; 33 + $url = $this->page->getUrl() . '/revisions/' . $this->id;
34 + if ($path) return $url . '/' . trim($path, '/');
35 + return $url;
36 + }
37 +
38 + /**
39 + * Get the previous revision for the same page if existing
40 + * @return \BookStack\PageRevision|null
41 + */
42 + public function getPrevious()
43 + {
44 + if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
45 + return static::find($id);
46 + }
47 + return null;
33 } 48 }
34 49
35 } 50 }
......
1 +<?php
2 +
3 +namespace BookStack\Providers;
4 +
5 +use Illuminate\Support\ServiceProvider;
6 +use Illuminate\Support\Facades\Broadcast;
7 +
8 +class BroadcastServiceProvider extends ServiceProvider
9 +{
10 + /**
11 + * Bootstrap any application services.
12 + *
13 + * @return void
14 + */
15 + public function boot()
16 + {
17 +// Broadcast::routes();
18 +//
19 +// /*
20 +// * Authenticate the user's personal channel...
21 +// */
22 +// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
23 +// return (int) $user->id === (int) $userId;
24 +// });
25 + }
26 +}
...@@ -21,13 +21,10 @@ class EventServiceProvider extends ServiceProvider ...@@ -21,13 +21,10 @@ class EventServiceProvider extends ServiceProvider
21 /** 21 /**
22 * Register any other events for your application. 22 * Register any other events for your application.
23 * 23 *
24 - * @param \Illuminate\Contracts\Events\Dispatcher $events
25 * @return void 24 * @return void
26 */ 25 */
27 - public function boot(DispatcherContract $events) 26 + public function boot()
28 { 27 {
29 - parent::boot($events); 28 + parent::boot();
30 -
31 - //
32 } 29 }
33 } 30 }
......
1 <?php namespace BookStack\Providers; 1 <?php namespace BookStack\Providers;
2 2
3 3
4 -use Illuminate\Support\ServiceProvider; 4 +use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
5 use Illuminate\Pagination\Paginator; 5 use Illuminate\Pagination\Paginator;
6 6
7 -class PaginationServiceProvider extends ServiceProvider 7 +class PaginationServiceProvider extends IlluminatePaginationServiceProvider
8 { 8 {
9 +
9 /** 10 /**
10 * Register the service provider. 11 * Register the service provider.
11 * 12 *
...@@ -13,6 +14,10 @@ class PaginationServiceProvider extends ServiceProvider ...@@ -13,6 +14,10 @@ class PaginationServiceProvider extends ServiceProvider
13 */ 14 */
14 public function register() 15 public function register()
15 { 16 {
17 + Paginator::viewFactoryResolver(function () {
18 + return $this->app['view'];
19 + });
20 +
16 Paginator::currentPathResolver(function () { 21 Paginator::currentPathResolver(function () {
17 return baseUrl($this->app['request']->path()); 22 return baseUrl($this->app['request']->path());
18 }); 23 });
......
...@@ -4,6 +4,7 @@ namespace BookStack\Providers; ...@@ -4,6 +4,7 @@ namespace BookStack\Providers;
4 4
5 use Illuminate\Routing\Router; 5 use Illuminate\Routing\Router;
6 use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; 6 use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
7 +use Route;
7 8
8 class RouteServiceProvider extends ServiceProvider 9 class RouteServiceProvider extends ServiceProvider
9 { 10 {
...@@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider ...@@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
19 /** 20 /**
20 * Define your route model bindings, pattern filters, etc. 21 * Define your route model bindings, pattern filters, etc.
21 * 22 *
22 - * @param \Illuminate\Routing\Router $router
23 * @return void 23 * @return void
24 */ 24 */
25 - public function boot(Router $router) 25 + public function boot()
26 { 26 {
27 - // 27 + parent::boot();
28 -
29 - parent::boot($router);
30 } 28 }
31 29
32 /** 30 /**
33 * Define the routes for the application. 31 * Define the routes for the application.
34 * 32 *
35 - * @param \Illuminate\Routing\Router $router
36 * @return void 33 * @return void
37 */ 34 */
38 - public function map(Router $router) 35 + public function map()
36 + {
37 + $this->mapWebRoutes();
38 +// $this->mapApiRoutes();
39 + }
40 + /**
41 + * Define the "web" routes for the application.
42 + *
43 + * These routes all receive session state, CSRF protection, etc.
44 + *
45 + * @return void
46 + */
47 + protected function mapWebRoutes()
48 + {
49 + Route::group([
50 + 'middleware' => 'web',
51 + 'namespace' => $this->namespace,
52 + ], function ($router) {
53 + require base_path('routes/web.php');
54 + });
55 + }
56 + /**
57 + * Define the "api" routes for the application.
58 + *
59 + * These routes are typically stateless.
60 + *
61 + * @return void
62 + */
63 + protected function mapApiRoutes()
39 { 64 {
40 - $router->group(['namespace' => $this->namespace], function ($router) { 65 + Route::group([
41 - require app_path('Http/routes.php'); 66 + 'middleware' => 'api',
67 + 'namespace' => $this->namespace,
68 + 'prefix' => 'api',
69 + ], function ($router) {
70 + require base_path('routes/api.php');
42 }); 71 });
43 } 72 }
44 } 73 }
......
...@@ -132,8 +132,8 @@ class BookRepo extends EntityRepo ...@@ -132,8 +132,8 @@ class BookRepo extends EntityRepo
132 { 132 {
133 $book = $this->book->newInstance($input); 133 $book = $this->book->newInstance($input);
134 $book->slug = $this->findSuitableSlug($book->name); 134 $book->slug = $this->findSuitableSlug($book->name);
135 - $book->created_by = auth()->user()->id; 135 + $book->created_by = user()->id;
136 - $book->updated_by = auth()->user()->id; 136 + $book->updated_by = user()->id;
137 $book->save(); 137 $book->save();
138 $this->permissionService->buildJointPermissionsForEntity($book); 138 $this->permissionService->buildJointPermissionsForEntity($book);
139 return $book; 139 return $book;
...@@ -147,9 +147,11 @@ class BookRepo extends EntityRepo ...@@ -147,9 +147,11 @@ class BookRepo extends EntityRepo
147 */ 147 */
148 public function updateFromInput(Book $book, $input) 148 public function updateFromInput(Book $book, $input)
149 { 149 {
150 + if ($book->name !== $input['name']) {
151 + $book->slug = $this->findSuitableSlug($input['name'], $book->id);
152 + }
150 $book->fill($input); 153 $book->fill($input);
151 - $book->slug = $this->findSuitableSlug($book->name, $book->id); 154 + $book->updated_by = user()->id;
152 - $book->updated_by = auth()->user()->id;
153 $book->save(); 155 $book->save();
154 $this->permissionService->buildJointPermissionsForEntity($book); 156 $this->permissionService->buildJointPermissionsForEntity($book);
155 return $book; 157 return $book;
...@@ -208,8 +210,7 @@ class BookRepo extends EntityRepo ...@@ -208,8 +210,7 @@ class BookRepo extends EntityRepo
208 */ 210 */
209 public function findSuitableSlug($name, $currentId = false) 211 public function findSuitableSlug($name, $currentId = false)
210 { 212 {
211 - $slug = Str::slug($name); 213 + $slug = $this->nameToSlug($name);
212 - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
213 while ($this->doesSlugExist($slug, $currentId)) { 214 while ($this->doesSlugExist($slug, $currentId)) {
214 $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); 215 $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
215 } 216 }
......
...@@ -98,8 +98,8 @@ class ChapterRepo extends EntityRepo ...@@ -98,8 +98,8 @@ class ChapterRepo extends EntityRepo
98 { 98 {
99 $chapter = $this->chapter->newInstance($input); 99 $chapter = $this->chapter->newInstance($input);
100 $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id); 100 $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
101 - $chapter->created_by = auth()->user()->id; 101 + $chapter->created_by = user()->id;
102 - $chapter->updated_by = auth()->user()->id; 102 + $chapter->updated_by = user()->id;
103 $chapter = $book->chapters()->save($chapter); 103 $chapter = $book->chapters()->save($chapter);
104 $this->permissionService->buildJointPermissionsForEntity($chapter); 104 $this->permissionService->buildJointPermissionsForEntity($chapter);
105 return $chapter; 105 return $chapter;
...@@ -150,8 +150,7 @@ class ChapterRepo extends EntityRepo ...@@ -150,8 +150,7 @@ class ChapterRepo extends EntityRepo
150 */ 150 */
151 public function findSuitableSlug($name, $bookId, $currentId = false) 151 public function findSuitableSlug($name, $bookId, $currentId = false)
152 { 152 {
153 - $slug = Str::slug($name); 153 + $slug = $this->nameToSlug($name);
154 - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
155 while ($this->doesSlugExist($slug, $bookId, $currentId)) { 154 while ($this->doesSlugExist($slug, $bookId, $currentId)) {
156 $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); 155 $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
157 } 156 }
......
...@@ -132,9 +132,8 @@ class EntityRepo ...@@ -132,9 +132,8 @@ class EntityRepo
132 */ 132 */
133 public function getUserDraftPages($count = 20, $page = 0) 133 public function getUserDraftPages($count = 20, $page = 0)
134 { 134 {
135 - $user = auth()->user();
136 return $this->page->where('draft', '=', true) 135 return $this->page->where('draft', '=', true)
137 - ->where('created_by', '=', $user->id) 136 + ->where('created_by', '=', user()->id)
138 ->orderBy('updated_at', 'desc') 137 ->orderBy('updated_at', 'desc')
139 ->skip($count * $page)->take($count)->get(); 138 ->skip($count * $page)->take($count)->get();
140 } 139 }
...@@ -270,6 +269,19 @@ class EntityRepo ...@@ -270,6 +269,19 @@ class EntityRepo
270 $this->permissionService->buildJointPermissionsForEntities($collection); 269 $this->permissionService->buildJointPermissionsForEntities($collection);
271 } 270 }
272 271
272 + /**
273 + * Format a name as a url slug.
274 + * @param $name
275 + * @return string
276 + */
277 + protected function nameToSlug($name)
278 + {
279 + $slug = str_replace(' ', '-', strtolower($name));
280 + $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
281 + if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
282 + return $slug;
283 + }
284 +
273 } 285 }
274 286
275 287
......
...@@ -5,6 +5,7 @@ use BookStack\Image; ...@@ -5,6 +5,7 @@ use BookStack\Image;
5 use BookStack\Page; 5 use BookStack\Page;
6 use BookStack\Services\ImageService; 6 use BookStack\Services\ImageService;
7 use BookStack\Services\PermissionService; 7 use BookStack\Services\PermissionService;
8 +use Illuminate\Contracts\Filesystem\FileNotFoundException;
8 use Setting; 9 use Setting;
9 use Symfony\Component\HttpFoundation\File\UploadedFile; 10 use Symfony\Component\HttpFoundation\File\UploadedFile;
10 11
...@@ -191,7 +192,12 @@ class ImageRepo ...@@ -191,7 +192,12 @@ class ImageRepo
191 */ 192 */
192 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) 193 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
193 { 194 {
194 - return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); 195 + try {
196 + return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
197 + } catch (FileNotFoundException $exception) {
198 + $image->delete();
199 + return [];
200 + }
195 } 201 }
196 202
197 203
......
...@@ -5,8 +5,10 @@ use BookStack\Book; ...@@ -5,8 +5,10 @@ use BookStack\Book;
5 use BookStack\Chapter; 5 use BookStack\Chapter;
6 use BookStack\Entity; 6 use BookStack\Entity;
7 use BookStack\Exceptions\NotFoundException; 7 use BookStack\Exceptions\NotFoundException;
8 +use BookStack\Services\AttachmentService;
8 use Carbon\Carbon; 9 use Carbon\Carbon;
9 use DOMDocument; 10 use DOMDocument;
11 +use DOMXPath;
10 use Illuminate\Support\Str; 12 use Illuminate\Support\Str;
11 use BookStack\Page; 13 use BookStack\Page;
12 use BookStack\PageRevision; 14 use BookStack\PageRevision;
...@@ -47,7 +49,7 @@ class PageRepo extends EntityRepo ...@@ -47,7 +49,7 @@ class PageRepo extends EntityRepo
47 * Get a page via a specific ID. 49 * Get a page via a specific ID.
48 * @param $id 50 * @param $id
49 * @param bool $allowDrafts 51 * @param bool $allowDrafts
50 - * @return mixed 52 + * @return Page
51 */ 53 */
52 public function getById($id, $allowDrafts = false) 54 public function getById($id, $allowDrafts = false)
53 { 55 {
...@@ -58,7 +60,7 @@ class PageRepo extends EntityRepo ...@@ -58,7 +60,7 @@ class PageRepo extends EntityRepo
58 * Get a page identified by the given slug. 60 * Get a page identified by the given slug.
59 * @param $slug 61 * @param $slug
60 * @param $bookId 62 * @param $bookId
61 - * @return mixed 63 + * @return Page
62 * @throws NotFoundException 64 * @throws NotFoundException
63 */ 65 */
64 public function getBySlug($slug, $bookId) 66 public function getBySlug($slug, $bookId)
...@@ -111,31 +113,6 @@ class PageRepo extends EntityRepo ...@@ -111,31 +113,6 @@ class PageRepo extends EntityRepo
111 } 113 }
112 114
113 /** 115 /**
114 - * Save a new page into the system.
115 - * Input validation must be done beforehand.
116 - * @param array $input
117 - * @param Book $book
118 - * @param int $chapterId
119 - * @return Page
120 - */
121 - public function saveNew(array $input, Book $book, $chapterId = null)
122 - {
123 - $page = $this->newFromInput($input);
124 - $page->slug = $this->findSuitableSlug($page->name, $book->id);
125 -
126 - if ($chapterId) $page->chapter_id = $chapterId;
127 -
128 - $page->html = $this->formatHtml($input['html']);
129 - $page->text = strip_tags($page->html);
130 - $page->created_by = auth()->user()->id;
131 - $page->updated_by = auth()->user()->id;
132 -
133 - $book->pages()->save($page);
134 - return $page;
135 - }
136 -
137 -
138 - /**
139 * Publish a draft page to make it a normal page. 116 * Publish a draft page to make it a normal page.
140 * Sets the slug and updates the content. 117 * Sets the slug and updates the content.
141 * @param Page $draftPage 118 * @param Page $draftPage
...@@ -172,8 +149,8 @@ class PageRepo extends EntityRepo ...@@ -172,8 +149,8 @@ class PageRepo extends EntityRepo
172 { 149 {
173 $page = $this->page->newInstance(); 150 $page = $this->page->newInstance();
174 $page->name = 'New Page'; 151 $page->name = 'New Page';
175 - $page->created_by = auth()->user()->id; 152 + $page->created_by = user()->id;
176 - $page->updated_by = auth()->user()->id; 153 + $page->updated_by = user()->id;
177 $page->draft = true; 154 $page->draft = true;
178 155
179 if ($chapter) $page->chapter_id = $chapter->id; 156 if ($chapter) $page->chapter_id = $chapter->id;
...@@ -184,6 +161,35 @@ class PageRepo extends EntityRepo ...@@ -184,6 +161,35 @@ class PageRepo extends EntityRepo
184 } 161 }
185 162
186 /** 163 /**
164 + * Parse te headers on the page to get a navigation menu
165 + * @param Page $page
166 + * @return array
167 + */
168 + public function getPageNav(Page $page)
169 + {
170 + if ($page->html == '') return null;
171 + libxml_use_internal_errors(true);
172 + $doc = new DOMDocument();
173 + $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8'));
174 + $xPath = new DOMXPath($doc);
175 + $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
176 +
177 + if (is_null($headers)) return null;
178 +
179 + $tree = [];
180 + foreach ($headers as $header) {
181 + $text = $header->nodeValue;
182 + $tree[] = [
183 + 'nodeName' => strtolower($header->nodeName),
184 + 'level' => intval(str_replace('h', '', $header->nodeName)),
185 + 'link' => '#' . $header->getAttribute('id'),
186 + 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
187 + ];
188 + }
189 + return $tree;
190 + }
191 +
192 + /**
187 * Formats a page's html to be tagged correctly 193 * Formats a page's html to be tagged correctly
188 * within the system. 194 * within the system.
189 * @param string $htmlText 195 * @param string $htmlText
...@@ -325,7 +331,7 @@ class PageRepo extends EntityRepo ...@@ -325,7 +331,7 @@ class PageRepo extends EntityRepo
325 } 331 }
326 332
327 // Update with new details 333 // Update with new details
328 - $userId = auth()->user()->id; 334 + $userId = user()->id;
329 $page->fill($input); 335 $page->fill($input);
330 $page->html = $this->formatHtml($input['html']); 336 $page->html = $this->formatHtml($input['html']);
331 $page->text = strip_tags($page->html); 337 $page->text = strip_tags($page->html);
...@@ -358,7 +364,7 @@ class PageRepo extends EntityRepo ...@@ -358,7 +364,7 @@ class PageRepo extends EntityRepo
358 $page->fill($revision->toArray()); 364 $page->fill($revision->toArray());
359 $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id); 365 $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
360 $page->text = strip_tags($page->html); 366 $page->text = strip_tags($page->html);
361 - $page->updated_by = auth()->user()->id; 367 + $page->updated_by = user()->id;
362 $page->save(); 368 $page->save();
363 return $page; 369 return $page;
364 } 370 }
...@@ -371,21 +377,23 @@ class PageRepo extends EntityRepo ...@@ -371,21 +377,23 @@ class PageRepo extends EntityRepo
371 */ 377 */
372 public function saveRevision(Page $page, $summary = null) 378 public function saveRevision(Page $page, $summary = null)
373 { 379 {
374 - $revision = $this->pageRevision->fill($page->toArray()); 380 + $revision = $this->pageRevision->newInstance($page->toArray());
375 if (setting('app-editor') !== 'markdown') $revision->markdown = ''; 381 if (setting('app-editor') !== 'markdown') $revision->markdown = '';
376 $revision->page_id = $page->id; 382 $revision->page_id = $page->id;
377 $revision->slug = $page->slug; 383 $revision->slug = $page->slug;
378 $revision->book_slug = $page->book->slug; 384 $revision->book_slug = $page->book->slug;
379 - $revision->created_by = auth()->user()->id; 385 + $revision->created_by = user()->id;
380 $revision->created_at = $page->updated_at; 386 $revision->created_at = $page->updated_at;
381 $revision->type = 'version'; 387 $revision->type = 'version';
382 $revision->summary = $summary; 388 $revision->summary = $summary;
383 $revision->save(); 389 $revision->save();
390 +
384 // Clear old revisions 391 // Clear old revisions
385 if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { 392 if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
386 $this->pageRevision->where('page_id', '=', $page->id) 393 $this->pageRevision->where('page_id', '=', $page->id)
387 ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); 394 ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
388 } 395 }
396 +
389 return $revision; 397 return $revision;
390 } 398 }
391 399
...@@ -397,7 +405,7 @@ class PageRepo extends EntityRepo ...@@ -397,7 +405,7 @@ class PageRepo extends EntityRepo
397 */ 405 */
398 public function saveUpdateDraft(Page $page, $data = []) 406 public function saveUpdateDraft(Page $page, $data = [])
399 { 407 {
400 - $userId = auth()->user()->id; 408 + $userId = user()->id;
401 $drafts = $this->userUpdateDraftsQuery($page, $userId)->get(); 409 $drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
402 410
403 if ($drafts->count() > 0) { 411 if ($drafts->count() > 0) {
...@@ -528,7 +536,7 @@ class PageRepo extends EntityRepo ...@@ -528,7 +536,7 @@ class PageRepo extends EntityRepo
528 $query = $this->pageRevision->where('type', '=', 'update_draft') 536 $query = $this->pageRevision->where('type', '=', 'update_draft')
529 ->where('page_id', '=', $page->id) 537 ->where('page_id', '=', $page->id)
530 ->where('updated_at', '>', $page->updated_at) 538 ->where('updated_at', '>', $page->updated_at)
531 - ->where('created_by', '!=', auth()->user()->id) 539 + ->where('created_by', '!=', user()->id)
532 ->with('createdBy'); 540 ->with('createdBy');
533 541
534 if ($minRange !== null) { 542 if ($minRange !== null) {
...@@ -541,7 +549,7 @@ class PageRepo extends EntityRepo ...@@ -541,7 +549,7 @@ class PageRepo extends EntityRepo
541 /** 549 /**
542 * Gets a single revision via it's id. 550 * Gets a single revision via it's id.
543 * @param $id 551 * @param $id
544 - * @return mixed 552 + * @return PageRevision
545 */ 553 */
546 public function getRevisionById($id) 554 public function getRevisionById($id)
547 { 555 {
...@@ -606,8 +614,7 @@ class PageRepo extends EntityRepo ...@@ -606,8 +614,7 @@ class PageRepo extends EntityRepo
606 */ 614 */
607 public function findSuitableSlug($name, $bookId, $currentId = false) 615 public function findSuitableSlug($name, $bookId, $currentId = false)
608 { 616 {
609 - $slug = Str::slug($name); 617 + $slug = $this->nameToSlug($name);
610 - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
611 while ($this->doesSlugExist($slug, $bookId, $currentId)) { 618 while ($this->doesSlugExist($slug, $bookId, $currentId)) {
612 $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); 619 $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
613 } 620 }
...@@ -626,12 +633,20 @@ class PageRepo extends EntityRepo ...@@ -626,12 +633,20 @@ class PageRepo extends EntityRepo
626 $page->revisions()->delete(); 633 $page->revisions()->delete();
627 $page->permissions()->delete(); 634 $page->permissions()->delete();
628 $this->permissionService->deleteJointPermissionsForEntity($page); 635 $this->permissionService->deleteJointPermissionsForEntity($page);
636 +
637 + // Delete AttachedFiles
638 + $attachmentService = app(AttachmentService::class);
639 + foreach ($page->attachments as $attachment) {
640 + $attachmentService->deleteFile($attachment);
641 + }
642 +
629 $page->delete(); 643 $page->delete();
630 } 644 }
631 645
632 /** 646 /**
633 * Get the latest pages added to the system. 647 * Get the latest pages added to the system.
634 * @param $count 648 * @param $count
649 + * @return mixed
635 */ 650 */
636 public function getRecentlyCreatedPaginated($count = 20) 651 public function getRecentlyCreatedPaginated($count = 20)
637 { 652 {
...@@ -641,6 +656,7 @@ class PageRepo extends EntityRepo ...@@ -641,6 +656,7 @@ class PageRepo extends EntityRepo
641 /** 656 /**
642 * Get the latest pages added to the system. 657 * Get the latest pages added to the system.
643 * @param $count 658 * @param $count
659 + * @return mixed
644 */ 660 */
645 public function getRecentlyUpdatedPaginated($count = 20) 661 public function getRecentlyUpdatedPaginated($count = 20)
646 { 662 {
......
...@@ -35,7 +35,7 @@ class PermissionsRepo ...@@ -35,7 +35,7 @@ class PermissionsRepo
35 */ 35 */
36 public function getAllRoles() 36 public function getAllRoles()
37 { 37 {
38 - return $this->role->where('hidden', '=', false)->get(); 38 + return $this->role->all();
39 } 39 }
40 40
41 /** 41 /**
...@@ -45,7 +45,7 @@ class PermissionsRepo ...@@ -45,7 +45,7 @@ class PermissionsRepo
45 */ 45 */
46 public function getAllRolesExcept(Role $role) 46 public function getAllRolesExcept(Role $role)
47 { 47 {
48 - return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get(); 48 + return $this->role->where('id', '!=', $role->id)->get();
49 } 49 }
50 50
51 /** 51 /**
...@@ -90,8 +90,6 @@ class PermissionsRepo ...@@ -90,8 +90,6 @@ class PermissionsRepo
90 { 90 {
91 $role = $this->role->findOrFail($roleId); 91 $role = $this->role->findOrFail($roleId);
92 92
93 - if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
94 -
95 $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; 93 $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
96 $this->assignRolePermissions($role, $permissions); 94 $this->assignRolePermissions($role, $permissions);
97 95
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 use BookStack\Role; 3 use BookStack\Role;
4 use BookStack\User; 4 use BookStack\User;
5 +use Exception;
5 use Setting; 6 use Setting;
6 7
7 class UserRepo 8 class UserRepo
...@@ -84,9 +85,14 @@ class UserRepo ...@@ -84,9 +85,14 @@ class UserRepo
84 85
85 // Get avatar from gravatar and save 86 // Get avatar from gravatar and save
86 if (!config('services.disable_services')) { 87 if (!config('services.disable_services')) {
87 - $avatar = \Images::saveUserGravatar($user); 88 + try {
88 - $user->avatar()->associate($avatar); 89 + $avatar = \Images::saveUserGravatar($user);
89 - $user->save(); 90 + $user->avatar()->associate($avatar);
91 + $user->save();
92 + } catch (Exception $e) {
93 + $user->save();
94 + \Log::error('Failed to save user gravatar image');
95 + }
90 } 96 }
91 97
92 return $user; 98 return $user;
...@@ -193,9 +199,9 @@ class UserRepo ...@@ -193,9 +199,9 @@ class UserRepo
193 * Get the roles in the system that are assignable to a user. 199 * Get the roles in the system that are assignable to a user.
194 * @return mixed 200 * @return mixed
195 */ 201 */
196 - public function getAssignableRoles() 202 + public function getAllRoles()
197 { 203 {
198 - return $this->role->visible(); 204 + return $this->role->all();
199 } 205 }
200 206
201 /** 207 /**
...@@ -205,7 +211,7 @@ class UserRepo ...@@ -205,7 +211,7 @@ class UserRepo
205 */ 211 */
206 public function getRestrictableRoles() 212 public function getRestrictableRoles()
207 { 213 {
208 - return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get(); 214 + return $this->role->where('system_name', '!=', 'admin')->get();
209 } 215 }
210 216
211 } 217 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -66,7 +66,7 @@ class Role extends Model ...@@ -66,7 +66,7 @@ class Role extends Model
66 /** 66 /**
67 * Get the role object for the specified role. 67 * Get the role object for the specified role.
68 * @param $roleName 68 * @param $roleName
69 - * @return mixed 69 + * @return Role
70 */ 70 */
71 public static function getRole($roleName) 71 public static function getRole($roleName)
72 { 72 {
...@@ -76,7 +76,7 @@ class Role extends Model ...@@ -76,7 +76,7 @@ class Role extends Model
76 /** 76 /**
77 * Get the role object for the specified system role. 77 * Get the role object for the specified system role.
78 * @param $roleName 78 * @param $roleName
79 - * @return mixed 79 + * @return Role
80 */ 80 */
81 public static function getSystemRole($roleName) 81 public static function getSystemRole($roleName)
82 { 82 {
......
...@@ -19,7 +19,7 @@ class ActivityService ...@@ -19,7 +19,7 @@ class ActivityService
19 { 19 {
20 $this->activity = $activity; 20 $this->activity = $activity;
21 $this->permissionService = $permissionService; 21 $this->permissionService = $permissionService;
22 - $this->user = auth()->user(); 22 + $this->user = user();
23 } 23 }
24 24
25 /** 25 /**
......
1 +<?php namespace BookStack\Services;
2 +
3 +use BookStack\Exceptions\FileUploadException;
4 +use BookStack\Attachment;
5 +use Exception;
6 +use Symfony\Component\HttpFoundation\File\UploadedFile;
7 +
8 +class AttachmentService extends UploadService
9 +{
10 +
11 + /**
12 + * Get an attachment from storage.
13 + * @param Attachment $attachment
14 + * @return string
15 + */
16 + public function getAttachmentFromStorage(Attachment $attachment)
17 + {
18 + $attachmentPath = $this->getStorageBasePath() . $attachment->path;
19 + return $this->getStorage()->get($attachmentPath);
20 + }
21 +
22 + /**
23 + * Store a new attachment upon user upload.
24 + * @param UploadedFile $uploadedFile
25 + * @param int $page_id
26 + * @return Attachment
27 + * @throws FileUploadException
28 + */
29 + public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
30 + {
31 + $attachmentName = $uploadedFile->getClientOriginalName();
32 + $attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
33 + $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
34 +
35 + $attachment = Attachment::forceCreate([
36 + 'name' => $attachmentName,
37 + 'path' => $attachmentPath,
38 + 'extension' => $uploadedFile->getClientOriginalExtension(),
39 + 'uploaded_to' => $page_id,
40 + 'created_by' => user()->id,
41 + 'updated_by' => user()->id,
42 + 'order' => $largestExistingOrder + 1
43 + ]);
44 +
45 + return $attachment;
46 + }
47 +
48 + /**
49 + * Store a upload, saving to a file and deleting any existing uploads
50 + * attached to that file.
51 + * @param UploadedFile $uploadedFile
52 + * @param Attachment $attachment
53 + * @return Attachment
54 + * @throws FileUploadException
55 + */
56 + public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
57 + {
58 + if (!$attachment->external) {
59 + $this->deleteFileInStorage($attachment);
60 + }
61 +
62 + $attachmentName = $uploadedFile->getClientOriginalName();
63 + $attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
64 +
65 + $attachment->name = $attachmentName;
66 + $attachment->path = $attachmentPath;
67 + $attachment->external = false;
68 + $attachment->extension = $uploadedFile->getClientOriginalExtension();
69 + $attachment->save();
70 + return $attachment;
71 + }
72 +
73 + /**
74 + * Save a new File attachment from a given link and name.
75 + * @param string $name
76 + * @param string $link
77 + * @param int $page_id
78 + * @return Attachment
79 + */
80 + public function saveNewFromLink($name, $link, $page_id)
81 + {
82 + $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
83 + return Attachment::forceCreate([
84 + 'name' => $name,
85 + 'path' => $link,
86 + 'external' => true,
87 + 'extension' => '',
88 + 'uploaded_to' => $page_id,
89 + 'created_by' => user()->id,
90 + 'updated_by' => user()->id,
91 + 'order' => $largestExistingOrder + 1
92 + ]);
93 + }
94 +
95 + /**
96 + * Get the file storage base path, amended for storage type.
97 + * This allows us to keep a generic path in the database.
98 + * @return string
99 + */
100 + private function getStorageBasePath()
101 + {
102 + return $this->isLocal() ? 'storage/' : '';
103 + }
104 +
105 + /**
106 + * Updates the file ordering for a listing of attached files.
107 + * @param array $attachmentList
108 + * @param $pageId
109 + */
110 + public function updateFileOrderWithinPage($attachmentList, $pageId)
111 + {
112 + foreach ($attachmentList as $index => $attachment) {
113 + Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
114 + }
115 + }
116 +
117 +
118 + /**
119 + * Update the details of a file.
120 + * @param Attachment $attachment
121 + * @param $requestData
122 + * @return Attachment
123 + */
124 + public function updateFile(Attachment $attachment, $requestData)
125 + {
126 + $attachment->name = $requestData['name'];
127 + if (isset($requestData['link']) && trim($requestData['link']) !== '') {
128 + $attachment->path = $requestData['link'];
129 + if (!$attachment->external) {
130 + $this->deleteFileInStorage($attachment);
131 + $attachment->external = true;
132 + }
133 + }
134 + $attachment->save();
135 + return $attachment;
136 + }
137 +
138 + /**
139 + * Delete a File from the database and storage.
140 + * @param Attachment $attachment
141 + */
142 + public function deleteFile(Attachment $attachment)
143 + {
144 + if ($attachment->external) {
145 + $attachment->delete();
146 + return;
147 + }
148 +
149 + $this->deleteFileInStorage($attachment);
150 + $attachment->delete();
151 + }
152 +
153 + /**
154 + * Delete a file from the filesystem it sits on.
155 + * Cleans any empty leftover folders.
156 + * @param Attachment $attachment
157 + */
158 + protected function deleteFileInStorage(Attachment $attachment)
159 + {
160 + $storedFilePath = $this->getStorageBasePath() . $attachment->path;
161 + $storage = $this->getStorage();
162 + $dirPath = dirname($storedFilePath);
163 +
164 + $storage->delete($storedFilePath);
165 + if (count($storage->allFiles($dirPath)) === 0) {
166 + $storage->deleteDirectory($dirPath);
167 + }
168 + }
169 +
170 + /**
171 + * Store a file in storage with the given filename
172 + * @param $attachmentName
173 + * @param UploadedFile $uploadedFile
174 + * @return string
175 + * @throws FileUploadException
176 + */
177 + protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
178 + {
179 + $attachmentData = file_get_contents($uploadedFile->getRealPath());
180 +
181 + $storage = $this->getStorage();
182 + $attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
183 + $storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
184 +
185 + $uploadFileName = $attachmentName;
186 + while ($storage->exists($storageBasePath . $uploadFileName)) {
187 + $uploadFileName = str_random(3) . $uploadFileName;
188 + }
189 +
190 + $attachmentPath = $attachmentBasePath . $uploadFileName;
191 + $attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
192 +
193 + try {
194 + $storage->put($attachmentStoragePath, $attachmentData);
195 + } catch (Exception $e) {
196 + throw new FileUploadException('File path ' . $attachmentStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
197 + }
198 + return $attachmentPath;
199 + }
200 +
201 +}
...\ No newline at end of file ...\ No newline at end of file
1 <?php namespace BookStack\Services; 1 <?php namespace BookStack\Services;
2 2
3 - 3 +use BookStack\Notifications\ConfirmEmail;
4 +use BookStack\Repos\UserRepo;
4 use Carbon\Carbon; 5 use Carbon\Carbon;
5 -use Illuminate\Contracts\Mail\Mailer;
6 -use Illuminate\Mail\Message;
7 -use BookStack\EmailConfirmation;
8 use BookStack\Exceptions\ConfirmationEmailException; 6 use BookStack\Exceptions\ConfirmationEmailException;
9 use BookStack\Exceptions\UserRegistrationException; 7 use BookStack\Exceptions\UserRegistrationException;
10 -use BookStack\Repos\UserRepo;
11 -use BookStack\Setting;
12 use BookStack\User; 8 use BookStack\User;
9 +use Illuminate\Database\Connection as Database;
13 10
14 class EmailConfirmationService 11 class EmailConfirmationService
15 { 12 {
16 - protected $mailer; 13 + protected $db;
17 - protected $emailConfirmation; 14 + protected $users;
18 15
19 /** 16 /**
20 * EmailConfirmationService constructor. 17 * EmailConfirmationService constructor.
21 - * @param Mailer $mailer 18 + * @param Database $db
22 - * @param EmailConfirmation $emailConfirmation 19 + * @param UserRepo $users
23 */ 20 */
24 - public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation) 21 + public function __construct(Database $db, UserRepo $users)
25 { 22 {
26 - $this->mailer = $mailer; 23 + $this->db = $db;
27 - $this->emailConfirmation = $emailConfirmation; 24 + $this->users = $users;
28 } 25 }
29 26
30 /** 27 /**
...@@ -38,16 +35,28 @@ class EmailConfirmationService ...@@ -38,16 +35,28 @@ class EmailConfirmationService
38 if ($user->email_confirmed) { 35 if ($user->email_confirmed) {
39 throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login'); 36 throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
40 } 37 }
38 +
41 $this->deleteConfirmationsByUser($user); 39 $this->deleteConfirmationsByUser($user);
40 + $token = $this->createEmailConfirmation($user);
41 +
42 + $user->notify(new ConfirmEmail($token));
43 + }
44 +
45 + /**
46 + * Creates a new email confirmation in the database and returns the token.
47 + * @param User $user
48 + * @return string
49 + */
50 + public function createEmailConfirmation(User $user)
51 + {
42 $token = $this->getToken(); 52 $token = $this->getToken();
43 - $this->emailConfirmation->create([ 53 + $this->db->table('email_confirmations')->insert([
44 'user_id' => $user->id, 54 'user_id' => $user->id,
45 - 'token' => $token, 55 + 'token' => $token,
56 + 'created_at' => Carbon::now(),
57 + 'updated_at' => Carbon::now()
46 ]); 58 ]);
47 - $this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) { 59 + return $token;
48 - $appName = setting('app-name', 'BookStack');
49 - $message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
50 - });
51 } 60 }
52 61
53 /** 62 /**
...@@ -59,22 +68,24 @@ class EmailConfirmationService ...@@ -59,22 +68,24 @@ class EmailConfirmationService
59 */ 68 */
60 public function getEmailConfirmationFromToken($token) 69 public function getEmailConfirmationFromToken($token)
61 { 70 {
62 - $emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first(); 71 + $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
63 - // If not found 72 +
73 + // If not found show error
64 if ($emailConfirmation === null) { 74 if ($emailConfirmation === null) {
65 throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register'); 75 throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
66 } 76 }
67 77
68 // If more than a day old 78 // If more than a day old
69 - if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) { 79 + if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
70 - $this->sendConfirmation($emailConfirmation->user); 80 + $user = $this->users->getById($emailConfirmation->user_id);
81 + $this->sendConfirmation($user);
71 throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm'); 82 throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
72 } 83 }
73 84
85 + $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
74 return $emailConfirmation; 86 return $emailConfirmation;
75 } 87 }
76 88
77 -
78 /** 89 /**
79 * Delete all email confirmations that belong to a user. 90 * Delete all email confirmations that belong to a user.
80 * @param User $user 91 * @param User $user
...@@ -82,7 +93,7 @@ class EmailConfirmationService ...@@ -82,7 +93,7 @@ class EmailConfirmationService
82 */ 93 */
83 public function deleteConfirmationsByUser(User $user) 94 public function deleteConfirmationsByUser(User $user)
84 { 95 {
85 - return $this->emailConfirmation->where('user_id', '=', $user->id)->delete(); 96 + return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
86 } 97 }
87 98
88 /** 99 /**
...@@ -92,7 +103,7 @@ class EmailConfirmationService ...@@ -92,7 +103,7 @@ class EmailConfirmationService
92 protected function getToken() 103 protected function getToken()
93 { 104 {
94 $token = str_random(24); 105 $token = str_random(24);
95 - while ($this->emailConfirmation->where('token', '=', $token)->exists()) { 106 + while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
96 $token = str_random(25); 107 $token = str_random(25);
97 } 108 }
98 return $token; 109 return $token;
......
...@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager; ...@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
9 use Illuminate\Contracts\Filesystem\Factory as FileSystem; 9 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
10 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; 10 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
11 use Illuminate\Contracts\Cache\Repository as Cache; 11 use Illuminate\Contracts\Cache\Repository as Cache;
12 -use Setting;
13 use Symfony\Component\HttpFoundation\File\UploadedFile; 12 use Symfony\Component\HttpFoundation\File\UploadedFile;
14 13
15 -class ImageService 14 +class ImageService extends UploadService
16 { 15 {
17 16
18 protected $imageTool; 17 protected $imageTool;
19 - protected $fileSystem;
20 protected $cache; 18 protected $cache;
21 -
22 - /**
23 - * @var FileSystemInstance
24 - */
25 - protected $storageInstance;
26 protected $storageUrl; 19 protected $storageUrl;
27 20
28 /** 21 /**
...@@ -34,8 +27,8 @@ class ImageService ...@@ -34,8 +27,8 @@ class ImageService
34 public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache) 27 public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
35 { 28 {
36 $this->imageTool = $imageTool; 29 $this->imageTool = $imageTool;
37 - $this->fileSystem = $fileSystem;
38 $this->cache = $cache; 30 $this->cache = $cache;
31 + parent::__construct($fileSystem);
39 } 32 }
40 33
41 /** 34 /**
...@@ -88,6 +81,9 @@ class ImageService ...@@ -88,6 +81,9 @@ class ImageService
88 if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; 81 if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
89 82
90 $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; 83 $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
84 +
85 + if ($this->isLocal()) $imagePath = '/public' . $imagePath;
86 +
91 while ($storage->exists($imagePath . $imageName)) { 87 while ($storage->exists($imagePath . $imageName)) {
92 $imageName = str_random(3) . $imageName; 88 $imageName = str_random(3) . $imageName;
93 } 89 }
...@@ -100,6 +96,8 @@ class ImageService ...@@ -100,6 +96,8 @@ class ImageService
100 throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); 96 throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
101 } 97 }
102 98
99 + if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
100 +
103 $imageDetails = [ 101 $imageDetails = [
104 'name' => $imageName, 102 'name' => $imageName,
105 'path' => $fullPath, 103 'path' => $fullPath,
...@@ -108,8 +106,8 @@ class ImageService ...@@ -108,8 +106,8 @@ class ImageService
108 'uploaded_to' => $uploadedTo 106 'uploaded_to' => $uploadedTo
109 ]; 107 ];
110 108
111 - if (auth()->user() && auth()->user()->id !== 0) { 109 + if (user()->id !== 0) {
112 - $userId = auth()->user()->id; 110 + $userId = user()->id;
113 $imageDetails['created_by'] = $userId; 111 $imageDetails['created_by'] = $userId;
114 $imageDetails['updated_by'] = $userId; 112 $imageDetails['updated_by'] = $userId;
115 } 113 }
...@@ -120,6 +118,16 @@ class ImageService ...@@ -120,6 +118,16 @@ class ImageService
120 } 118 }
121 119
122 /** 120 /**
121 + * Get the storage path, Dependant of storage type.
122 + * @param Image $image
123 + * @return mixed|string
124 + */
125 + protected function getPath(Image $image)
126 + {
127 + return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
128 + }
129 +
130 + /**
123 * Get the thumbnail for an image. 131 * Get the thumbnail for an image.
124 * If $keepRatio is true only the width will be used. 132 * If $keepRatio is true only the width will be used.
125 * Checks the cache then storage to avoid creating / accessing the filesystem on every check. 133 * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
...@@ -135,7 +143,8 @@ class ImageService ...@@ -135,7 +143,8 @@ class ImageService
135 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) 143 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
136 { 144 {
137 $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; 145 $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
138 - $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); 146 + $imagePath = $this->getPath($image);
147 + $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
139 148
140 if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { 149 if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
141 return $this->getPublicUrl($thumbFilePath); 150 return $this->getPublicUrl($thumbFilePath);
...@@ -148,7 +157,7 @@ class ImageService ...@@ -148,7 +157,7 @@ class ImageService
148 } 157 }
149 158
150 try { 159 try {
151 - $thumb = $this->imageTool->make($storage->get($image->path)); 160 + $thumb = $this->imageTool->make($storage->get($imagePath));
152 } catch (Exception $e) { 161 } catch (Exception $e) {
153 if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { 162 if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
154 throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); 163 throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
...@@ -183,8 +192,8 @@ class ImageService ...@@ -183,8 +192,8 @@ class ImageService
183 { 192 {
184 $storage = $this->getStorage(); 193 $storage = $this->getStorage();
185 194
186 - $imageFolder = dirname($image->path); 195 + $imageFolder = dirname($this->getPath($image));
187 - $imageFileName = basename($image->path); 196 + $imageFileName = basename($this->getPath($image));
188 $allImages = collect($storage->allFiles($imageFolder)); 197 $allImages = collect($storage->allFiles($imageFolder));
189 198
190 $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { 199 $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
...@@ -213,7 +222,7 @@ class ImageService ...@@ -213,7 +222,7 @@ class ImageService
213 public function saveUserGravatar(User $user, $size = 500) 222 public function saveUserGravatar(User $user, $size = 500)
214 { 223 {
215 $emailHash = md5(strtolower(trim($user->email))); 224 $emailHash = md5(strtolower(trim($user->email)));
216 - $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; 225 + $url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
217 $imageName = str_replace(' ', '-', $user->name . '-gravatar.png'); 226 $imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
218 $image = $this->saveNewFromUrl($url, 'user', $imageName); 227 $image = $this->saveNewFromUrl($url, 'user', $imageName);
219 $image->created_by = $user->id; 228 $image->created_by = $user->id;
...@@ -223,34 +232,8 @@ class ImageService ...@@ -223,34 +232,8 @@ class ImageService
223 } 232 }
224 233
225 /** 234 /**
226 - * Get the storage that will be used for storing images.
227 - * @return FileSystemInstance
228 - */
229 - private function getStorage()
230 - {
231 - if ($this->storageInstance !== null) return $this->storageInstance;
232 -
233 - $storageType = config('filesystems.default');
234 - $this->storageInstance = $this->fileSystem->disk($storageType);
235 -
236 - return $this->storageInstance;
237 - }
238 -
239 - /**
240 - * Check whether or not a folder is empty.
241 - * @param $path
242 - * @return int
243 - */
244 - private function isFolderEmpty($path)
245 - {
246 - $files = $this->getStorage()->files($path);
247 - $folders = $this->getStorage()->directories($path);
248 - return count($files) === 0 && count($folders) === 0;
249 - }
250 -
251 - /**
252 * Gets a public facing url for an image by checking relevant environment variables. 235 * Gets a public facing url for an image by checking relevant environment variables.
253 - * @param $filePath 236 + * @param string $filePath
254 * @return string 237 * @return string
255 */ 238 */
256 private function getPublicUrl($filePath) 239 private function getPublicUrl($filePath)
...@@ -273,6 +256,8 @@ class ImageService ...@@ -273,6 +256,8 @@ class ImageService
273 $this->storageUrl = $storageUrl; 256 $this->storageUrl = $storageUrl;
274 } 257 }
275 258
259 + if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
260 +
276 return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath; 261 return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
277 } 262 }
278 263
......
...@@ -9,14 +9,15 @@ use BookStack\Page; ...@@ -9,14 +9,15 @@ use BookStack\Page;
9 use BookStack\Role; 9 use BookStack\Role;
10 use BookStack\User; 10 use BookStack\User;
11 use Illuminate\Support\Collection; 11 use Illuminate\Support\Collection;
12 +use Illuminate\Support\Facades\Log;
12 13
13 class PermissionService 14 class PermissionService
14 { 15 {
15 16
16 - protected $userRoles;
17 - protected $isAdmin;
18 protected $currentAction; 17 protected $currentAction;
19 - protected $currentUser; 18 + protected $isAdminUser;
19 + protected $userRoles = false;
20 + protected $currentUserModel = false;
20 21
21 public $book; 22 public $book;
22 public $chapter; 23 public $chapter;
...@@ -37,12 +38,6 @@ class PermissionService ...@@ -37,12 +38,6 @@ class PermissionService
37 */ 38 */
38 public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role) 39 public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
39 { 40 {
40 - $this->currentUser = auth()->user();
41 - $userSet = $this->currentUser !== null;
42 - $this->userRoles = false;
43 - $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
44 - if (!$userSet) $this->currentUser = new User();
45 -
46 $this->jointPermission = $jointPermission; 41 $this->jointPermission = $jointPermission;
47 $this->role = $role; 42 $this->role = $role;
48 $this->book = $book; 43 $this->book = $book;
...@@ -117,7 +112,7 @@ class PermissionService ...@@ -117,7 +112,7 @@ class PermissionService
117 } 112 }
118 113
119 114
120 - foreach ($this->currentUser->roles as $role) { 115 + foreach ($this->currentUser()->roles as $role) {
121 $roles[] = $role->id; 116 $roles[] = $role->id;
122 } 117 }
123 return $roles; 118 return $roles;
...@@ -389,7 +384,11 @@ class PermissionService ...@@ -389,7 +384,11 @@ class PermissionService
389 */ 384 */
390 public function checkOwnableUserAccess(Ownable $ownable, $permission) 385 public function checkOwnableUserAccess(Ownable $ownable, $permission)
391 { 386 {
392 - if ($this->isAdmin) return true; 387 + if ($this->isAdmin()) {
388 + $this->clean();
389 + return true;
390 + }
391 +
393 $explodedPermission = explode('-', $permission); 392 $explodedPermission = explode('-', $permission);
394 393
395 $baseQuery = $ownable->where('id', '=', $ownable->id); 394 $baseQuery = $ownable->where('id', '=', $ownable->id);
...@@ -400,10 +399,10 @@ class PermissionService ...@@ -400,10 +399,10 @@ class PermissionService
400 399
401 // Handle non entity specific jointPermissions 400 // Handle non entity specific jointPermissions
402 if (in_array($explodedPermission[0], $nonJointPermissions)) { 401 if (in_array($explodedPermission[0], $nonJointPermissions)) {
403 - $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all'); 402 + $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
404 - $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own'); 403 + $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
405 $this->currentAction = 'view'; 404 $this->currentAction = 'view';
406 - $isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by; 405 + $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
407 return ($allPermission || ($isOwner && $ownPermission)); 406 return ($allPermission || ($isOwner && $ownPermission));
408 } 407 }
409 408
...@@ -413,7 +412,9 @@ class PermissionService ...@@ -413,7 +412,9 @@ class PermissionService
413 } 412 }
414 413
415 414
416 - return $this->entityRestrictionQuery($baseQuery)->count() > 0; 415 + $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
416 + $this->clean();
417 + return $q;
417 } 418 }
418 419
419 /** 420 /**
...@@ -443,7 +444,7 @@ class PermissionService ...@@ -443,7 +444,7 @@ class PermissionService
443 */ 444 */
444 protected function entityRestrictionQuery($query) 445 protected function entityRestrictionQuery($query)
445 { 446 {
446 - return $query->where(function ($parentQuery) { 447 + $q = $query->where(function ($parentQuery) {
447 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) { 448 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
448 $permissionQuery->whereIn('role_id', $this->getRoles()) 449 $permissionQuery->whereIn('role_id', $this->getRoles())
449 ->where('action', '=', $this->currentAction) 450 ->where('action', '=', $this->currentAction)
...@@ -451,11 +452,13 @@ class PermissionService ...@@ -451,11 +452,13 @@ class PermissionService
451 $query->where('has_permission', '=', true) 452 $query->where('has_permission', '=', true)
452 ->orWhere(function ($query) { 453 ->orWhere(function ($query) {
453 $query->where('has_permission_own', '=', true) 454 $query->where('has_permission_own', '=', true)
454 - ->where('created_by', '=', $this->currentUser->id); 455 + ->where('created_by', '=', $this->currentUser()->id);
455 }); 456 });
456 }); 457 });
457 }); 458 });
458 }); 459 });
460 + $this->clean();
461 + return $q;
459 } 462 }
460 463
461 /** 464 /**
...@@ -469,9 +472,9 @@ class PermissionService ...@@ -469,9 +472,9 @@ class PermissionService
469 // Prevent drafts being visible to others. 472 // Prevent drafts being visible to others.
470 $query = $query->where(function ($query) { 473 $query = $query->where(function ($query) {
471 $query->where('draft', '=', false); 474 $query->where('draft', '=', false);
472 - if ($this->currentUser) { 475 + if ($this->currentUser()) {
473 $query->orWhere(function ($query) { 476 $query->orWhere(function ($query) {
474 - $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id); 477 + $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
475 }); 478 });
476 } 479 }
477 }); 480 });
...@@ -509,7 +512,10 @@ class PermissionService ...@@ -509,7 +512,10 @@ class PermissionService
509 */ 512 */
510 public function enforceEntityRestrictions($query, $action = 'view') 513 public function enforceEntityRestrictions($query, $action = 'view')
511 { 514 {
512 - if ($this->isAdmin) return $query; 515 + if ($this->isAdmin()) {
516 + $this->clean();
517 + return $query;
518 + }
513 $this->currentAction = $action; 519 $this->currentAction = $action;
514 return $this->entityRestrictionQuery($query); 520 return $this->entityRestrictionQuery($query);
515 } 521 }
...@@ -524,11 +530,15 @@ class PermissionService ...@@ -524,11 +530,15 @@ class PermissionService
524 */ 530 */
525 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn) 531 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
526 { 532 {
527 - if ($this->isAdmin) return $query; 533 + if ($this->isAdmin()) {
534 + $this->clean();
535 + return $query;
536 + }
537 +
528 $this->currentAction = 'view'; 538 $this->currentAction = 'view';
529 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; 539 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
530 540
531 - return $query->where(function ($query) use ($tableDetails) { 541 + $q = $query->where(function ($query) use ($tableDetails) {
532 $query->whereExists(function ($permissionQuery) use (&$tableDetails) { 542 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
533 $permissionQuery->select('id')->from('joint_permissions') 543 $permissionQuery->select('id')->from('joint_permissions')
534 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) 544 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
...@@ -538,12 +548,12 @@ class PermissionService ...@@ -538,12 +548,12 @@ class PermissionService
538 ->where(function ($query) { 548 ->where(function ($query) {
539 $query->where('has_permission', '=', true)->orWhere(function ($query) { 549 $query->where('has_permission', '=', true)->orWhere(function ($query) {
540 $query->where('has_permission_own', '=', true) 550 $query->where('has_permission_own', '=', true)
541 - ->where('created_by', '=', $this->currentUser->id); 551 + ->where('created_by', '=', $this->currentUser()->id);
542 }); 552 });
543 }); 553 });
544 }); 554 });
545 }); 555 });
546 - 556 + return $q;
547 } 557 }
548 558
549 /** 559 /**
...@@ -555,11 +565,15 @@ class PermissionService ...@@ -555,11 +565,15 @@ class PermissionService
555 */ 565 */
556 public function filterRelatedPages($query, $tableName, $entityIdColumn) 566 public function filterRelatedPages($query, $tableName, $entityIdColumn)
557 { 567 {
558 - if ($this->isAdmin) return $query; 568 + if ($this->isAdmin()) {
569 + $this->clean();
570 + return $query;
571 + }
572 +
559 $this->currentAction = 'view'; 573 $this->currentAction = 'view';
560 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; 574 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
561 575
562 - return $query->where(function ($query) use ($tableDetails) { 576 + $q = $query->where(function ($query) use ($tableDetails) {
563 $query->where(function ($query) use (&$tableDetails) { 577 $query->where(function ($query) use (&$tableDetails) {
564 $query->whereExists(function ($permissionQuery) use (&$tableDetails) { 578 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
565 $permissionQuery->select('id')->from('joint_permissions') 579 $permissionQuery->select('id')->from('joint_permissions')
...@@ -570,12 +584,50 @@ class PermissionService ...@@ -570,12 +584,50 @@ class PermissionService
570 ->where(function ($query) { 584 ->where(function ($query) {
571 $query->where('has_permission', '=', true)->orWhere(function ($query) { 585 $query->where('has_permission', '=', true)->orWhere(function ($query) {
572 $query->where('has_permission_own', '=', true) 586 $query->where('has_permission_own', '=', true)
573 - ->where('created_by', '=', $this->currentUser->id); 587 + ->where('created_by', '=', $this->currentUser()->id);
574 }); 588 });
575 }); 589 });
576 }); 590 });
577 })->orWhere($tableDetails['entityIdColumn'], '=', 0); 591 })->orWhere($tableDetails['entityIdColumn'], '=', 0);
578 }); 592 });
593 + $this->clean();
594 + return $q;
595 + }
596 +
597 + /**
598 + * Check if the current user is an admin.
599 + * @return bool
600 + */
601 + private function isAdmin()
602 + {
603 + if ($this->isAdminUser === null) {
604 + $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false;
605 + }
606 +
607 + return $this->isAdminUser;
608 + }
609 +
610 + /**
611 + * Get the current user
612 + * @return User
613 + */
614 + private function currentUser()
615 + {
616 + if ($this->currentUserModel === false) {
617 + $this->currentUserModel = user();
618 + }
619 +
620 + return $this->currentUserModel;
621 + }
622 +
623 + /**
624 + * Clean the cached user elements.
625 + */
626 + private function clean()
627 + {
628 + $this->currentUserModel = false;
629 + $this->userRoles = false;
630 + $this->isAdminUser = null;
579 } 631 }
580 632
581 } 633 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -100,7 +100,7 @@ class SocialAuthService ...@@ -100,7 +100,7 @@ class SocialAuthService
100 $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first(); 100 $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
101 $user = $this->userRepo->getByEmail($socialUser->getEmail()); 101 $user = $this->userRepo->getByEmail($socialUser->getEmail());
102 $isLoggedIn = auth()->check(); 102 $isLoggedIn = auth()->check();
103 - $currentUser = auth()->user(); 103 + $currentUser = user();
104 104
105 // When a user is not logged in and a matching SocialAccount exists, 105 // When a user is not logged in and a matching SocialAccount exists,
106 // Simply log the user into the application. 106 // Simply log the user into the application.
...@@ -214,9 +214,9 @@ class SocialAuthService ...@@ -214,9 +214,9 @@ class SocialAuthService
214 public function detachSocialAccount($socialDriver) 214 public function detachSocialAccount($socialDriver)
215 { 215 {
216 session(); 216 session();
217 - auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete(); 217 + user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
218 session()->flash('success', title_case($socialDriver) . ' account successfully detached'); 218 session()->flash('success', title_case($socialDriver) . ' account successfully detached');
219 - return redirect(auth()->user()->getEditUrl()); 219 + return redirect(user()->getEditUrl());
220 } 220 }
221 221
222 } 222 }
...\ No newline at end of file ...\ No newline at end of file
......
1 +<?php namespace BookStack\Services;
2 +
3 +use Illuminate\Contracts\Filesystem\Factory as FileSystem;
4 +use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
5 +
6 +class UploadService
7 +{
8 +
9 + /**
10 + * @var FileSystem
11 + */
12 + protected $fileSystem;
13 +
14 + /**
15 + * @var FileSystemInstance
16 + */
17 + protected $storageInstance;
18 +
19 +
20 + /**
21 + * FileService constructor.
22 + * @param $fileSystem
23 + */
24 + public function __construct(FileSystem $fileSystem)
25 + {
26 + $this->fileSystem = $fileSystem;
27 + }
28 +
29 + /**
30 + * Get the storage that will be used for storing images.
31 + * @return FileSystemInstance
32 + */
33 + protected function getStorage()
34 + {
35 + if ($this->storageInstance !== null) return $this->storageInstance;
36 +
37 + $storageType = config('filesystems.default');
38 + $this->storageInstance = $this->fileSystem->disk($storageType);
39 +
40 + return $this->storageInstance;
41 + }
42 +
43 +
44 + /**
45 + * Check whether or not a folder is empty.
46 + * @param $path
47 + * @return bool
48 + */
49 + protected function isFolderEmpty($path)
50 + {
51 + $files = $this->getStorage()->files($path);
52 + $folders = $this->getStorage()->directories($path);
53 + return (count($files) === 0 && count($folders) === 0);
54 + }
55 +
56 + /**
57 + * Check if using a local filesystem.
58 + * @return bool
59 + */
60 + protected function isLocal()
61 + {
62 + return strtolower(config('filesystems.default')) === 'local';
63 + }
64 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -18,7 +18,7 @@ class ViewService ...@@ -18,7 +18,7 @@ class ViewService
18 public function __construct(View $view, PermissionService $permissionService) 18 public function __construct(View $view, PermissionService $permissionService)
19 { 19 {
20 $this->view = $view; 20 $this->view = $view;
21 - $this->user = auth()->user(); 21 + $this->user = user();
22 $this->permissionService = $permissionService; 22 $this->permissionService = $permissionService;
23 } 23 }
24 24
...@@ -84,7 +84,7 @@ class ViewService ...@@ -84,7 +84,7 @@ class ViewService
84 ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); 84 ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
85 85
86 if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); 86 if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
87 - $query = $query->where('user_id', '=', auth()->user()->id); 87 + $query = $query->where('user_id', '=', user()->id);
88 88
89 $viewables = $query->with('viewable')->orderBy('updated_at', 'desc') 89 $viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
90 ->skip($count * $page)->take($count)->get()->pluck('viewable'); 90 ->skip($count * $page)->take($count)->get()->pluck('viewable');
......
1 <?php namespace BookStack; 1 <?php namespace BookStack;
2 2
3 +use BookStack\Notifications\ResetPassword;
3 use Illuminate\Auth\Authenticatable; 4 use Illuminate\Auth\Authenticatable;
4 use Illuminate\Auth\Passwords\CanResetPassword; 5 use Illuminate\Auth\Passwords\CanResetPassword;
5 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; 6 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
6 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; 7 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
8 +use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9 +use Illuminate\Notifications\Notifiable;
7 10
8 class User extends Model implements AuthenticatableContract, CanResetPasswordContract 11 class User extends Model implements AuthenticatableContract, CanResetPasswordContract
9 { 12 {
10 - use Authenticatable, CanResetPassword; 13 + use Authenticatable, CanResetPassword, Notifiable;
11 14
12 /** 15 /**
13 * The database table used by the model. 16 * The database table used by the model.
...@@ -34,21 +37,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -34,21 +37,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
34 protected $permissions; 37 protected $permissions;
35 38
36 /** 39 /**
37 - * Returns a default guest user. 40 + * Returns the default public user.
41 + * @return User
38 */ 42 */
39 public static function getDefault() 43 public static function getDefault()
40 { 44 {
41 - return new static([ 45 + return static::where('system_name', '=', 'public')->first();
42 - 'email' => 'guest', 46 + }
43 - 'name' => 'Guest' 47 +
44 - ]); 48 + /**
49 + * Check if the user is the default public user.
50 + * @return bool
51 + */
52 + public function isDefault()
53 + {
54 + return $this->system_name === 'public';
45 } 55 }
46 56
47 /** 57 /**
48 * The roles that belong to the user. 58 * The roles that belong to the user.
59 + * @return BelongsToMany
49 */ 60 */
50 public function roles() 61 public function roles()
51 { 62 {
63 + if ($this->id === 0) return ;
52 return $this->belongsToMany(Role::class); 64 return $this->belongsToMany(Role::class);
53 } 65 }
54 66
...@@ -183,4 +195,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -183,4 +195,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
183 195
184 return ''; 196 return '';
185 } 197 }
198 +
199 + /**
200 + * Send the password reset notification.
201 + * @param string $token
202 + * @return void
203 + */
204 + public function sendPasswordResetNotification($token)
205 + {
206 + $this->notify(new ResetPassword($token));
207 + }
186 } 208 }
......
...@@ -11,29 +11,30 @@ use BookStack\Ownable; ...@@ -11,29 +11,30 @@ use BookStack\Ownable;
11 */ 11 */
12 function versioned_asset($file = '') 12 function versioned_asset($file = '')
13 { 13 {
14 - // Don't require css and JS assets for testing 14 + static $version = null;
15 - if (config('app.env') === 'testing') return ''; 15 +
16 - 16 + if (is_null($version)) {
17 - static $manifest = null; 17 + $versionFile = base_path('version');
18 - $manifestPath = 'build/manifest.json'; 18 + $version = trim(file_get_contents($versionFile));
19 -
20 - if (is_null($manifest) && file_exists($manifestPath)) {
21 - $manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
22 - } else if (!file_exists($manifestPath)) {
23 - if (config('app.env') !== 'production') {
24 - $path = public_path($manifestPath);
25 - $error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
26 - } else {
27 - $error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
28 - }
29 - throw new \Exception($error);
30 } 19 }
31 20
32 - if (isset($manifest[$file])) { 21 + $additional = '';
33 - return baseUrl($manifest[$file]); 22 + if (config('app.env') === 'development') {
23 + $additional = sha1_file(public_path($file));
34 } 24 }
35 25
36 - throw new InvalidArgumentException("File {$file} not defined in asset manifest."); 26 + $path = $file . '?version=' . urlencode($version) . $additional;
27 + return baseUrl($path);
28 +}
29 +
30 +/**
31 + * Helper method to get the current User.
32 + * Defaults to public 'Guest' user if not logged in.
33 + * @return \BookStack\User
34 + */
35 +function user()
36 +{
37 + return auth()->user() ?: \BookStack\User::getDefault();
37 } 38 }
38 39
39 /** 40 /**
...@@ -47,7 +48,7 @@ function versioned_asset($file = '') ...@@ -47,7 +48,7 @@ function versioned_asset($file = '')
47 function userCan($permission, Ownable $ownable = null) 48 function userCan($permission, Ownable $ownable = null)
48 { 49 {
49 if ($ownable === null) { 50 if ($ownable === null) {
50 - return auth()->user() && auth()->user()->can($permission); 51 + return user() && user()->can($permission);
51 } 52 }
52 53
53 // Check permission on ownable item 54 // Check permission on ownable item
...@@ -63,7 +64,7 @@ function userCan($permission, Ownable $ownable = null) ...@@ -63,7 +64,7 @@ function userCan($permission, Ownable $ownable = null)
63 */ 64 */
64 function setting($key, $default = false) 65 function setting($key, $default = false)
65 { 66 {
66 - $settingService = app('BookStack\Services\SettingService'); 67 + $settingService = app(\BookStack\Services\SettingService::class);
67 return $settingService->get($key, $default); 68 return $settingService->get($key, $default);
68 } 69 }
69 70
...@@ -79,6 +80,7 @@ function baseUrl($path, $forceAppDomain = false) ...@@ -79,6 +80,7 @@ function baseUrl($path, $forceAppDomain = false)
79 if ($isFullUrl && !$forceAppDomain) return $path; 80 if ($isFullUrl && !$forceAppDomain) return $path;
80 $path = trim($path, '/'); 81 $path = trim($path, '/');
81 82
83 + // Remove non-specified domain if forced and we have a domain
82 if ($isFullUrl && $forceAppDomain) { 84 if ($isFullUrl && $forceAppDomain) {
83 $explodedPath = explode('/', $path); 85 $explodedPath = explode('/', $path);
84 $path = implode('/', array_splice($explodedPath, 3)); 86 $path = implode('/', array_splice($explodedPath, 3));
...@@ -127,14 +129,14 @@ function sortUrl($path, $data, $overrideData = []) ...@@ -127,14 +129,14 @@ function sortUrl($path, $data, $overrideData = [])
127 { 129 {
128 $queryStringSections = []; 130 $queryStringSections = [];
129 $queryData = array_merge($data, $overrideData); 131 $queryData = array_merge($data, $overrideData);
130 - 132 +
131 // Change sorting direction is already sorted on current attribute 133 // Change sorting direction is already sorted on current attribute
132 if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) { 134 if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
133 $queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc'; 135 $queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
134 } else { 136 } else {
135 $queryData['order'] = 'asc'; 137 $queryData['order'] = 'asc';
136 } 138 }
137 - 139 +
138 foreach ($queryData as $name => $value) { 140 foreach ($queryData as $name => $value) {
139 $trimmedVal = trim($value); 141 $trimmedVal = trim($value);
140 if ($trimmedVal === '') continue; 142 if ($trimmedVal === '') continue;
...@@ -144,4 +146,4 @@ function sortUrl($path, $data, $overrideData = []) ...@@ -144,4 +146,4 @@ function sortUrl($path, $data, $overrideData = [])
144 if (count($queryStringSections) === 0) return $path; 146 if (count($queryStringSections) === 0) return $path;
145 147
146 return baseUrl($path . '?' . implode('&', $queryStringSections)); 148 return baseUrl($path . '?' . implode('&', $queryStringSections));
147 -}
...\ No newline at end of file ...\ No newline at end of file
149 +}
......
...@@ -5,23 +5,24 @@ ...@@ -5,23 +5,24 @@
5 "license": "MIT", 5 "license": "MIT",
6 "type": "project", 6 "type": "project",
7 "require": { 7 "require": {
8 - "php": ">=5.5.9", 8 + "php": ">=5.6.4",
9 - "laravel/framework": "5.2.*", 9 + "laravel/framework": "^5.3.4",
10 + "ext-tidy": "*",
10 "intervention/image": "^2.3", 11 "intervention/image": "^2.3",
11 "laravel/socialite": "^2.0", 12 "laravel/socialite": "^2.0",
12 "barryvdh/laravel-ide-helper": "^2.1", 13 "barryvdh/laravel-ide-helper": "^2.1",
13 - "barryvdh/laravel-debugbar": "^2.0", 14 + "barryvdh/laravel-debugbar": "^2.2.3",
14 "league/flysystem-aws-s3-v3": "^1.0", 15 "league/flysystem-aws-s3-v3": "^1.0",
15 - "barryvdh/laravel-dompdf": "0.6.*", 16 + "barryvdh/laravel-dompdf": "^0.7",
16 - "predis/predis": "^1.0" 17 + "predis/predis": "^1.1",
18 + "gathercontent/htmldiff": "^0.2.1"
17 }, 19 },
18 "require-dev": { 20 "require-dev": {
19 "fzaninotto/faker": "~1.4", 21 "fzaninotto/faker": "~1.4",
20 "mockery/mockery": "0.9.*", 22 "mockery/mockery": "0.9.*",
21 - "phpunit/phpunit": "~4.0", 23 + "phpunit/phpunit": "~5.0",
22 - "phpspec/phpspec": "~2.1", 24 + "symfony/css-selector": "3.1.*",
23 - "symfony/dom-crawler": "~3.0", 25 + "symfony/dom-crawler": "3.1.*"
24 - "symfony/css-selector": "~3.0"
25 }, 26 },
26 "autoload": { 27 "autoload": {
27 "classmap": [ 28 "classmap": [
...@@ -37,21 +38,19 @@ ...@@ -37,21 +38,19 @@
37 ] 38 ]
38 }, 39 },
39 "scripts": { 40 "scripts": {
41 + "post-root-package-install": [
42 + "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
43 + ],
44 + "post-create-project-cmd": [
45 + "php artisan key:generate"
46 + ],
40 "post-install-cmd": [ 47 "post-install-cmd": [
41 - "php artisan clear-compiled", 48 + "Illuminate\\Foundation\\ComposerScripts::postInstall",
42 "php artisan optimize" 49 "php artisan optimize"
43 ], 50 ],
44 - "pre-update-cmd": [
45 - "php artisan clear-compiled"
46 - ],
47 "post-update-cmd": [ 51 "post-update-cmd": [
52 + "Illuminate\\Foundation\\ComposerScripts::postUpdate",
48 "php artisan optimize" 53 "php artisan optimize"
49 - ],
50 - "post-root-package-install": [
51 - "php -r \"copy('.env.example', '.env');\""
52 - ],
53 - "post-create-project-cmd": [
54 - "php artisan key:generate"
55 ] 54 ]
56 }, 55 },
57 "config": { 56 "config": {
......
This diff could not be displayed because it is too large.
...@@ -57,7 +57,7 @@ return [ ...@@ -57,7 +57,7 @@ return [
57 | 57 |
58 */ 58 */
59 59
60 - 'locale' => 'en', 60 + 'locale' => env('APP_LANG', 'en'),
61 61
62 /* 62 /*
63 |-------------------------------------------------------------------------- 63 |--------------------------------------------------------------------------
...@@ -138,6 +138,7 @@ return [ ...@@ -138,6 +138,7 @@ return [
138 Illuminate\Translation\TranslationServiceProvider::class, 138 Illuminate\Translation\TranslationServiceProvider::class,
139 Illuminate\Validation\ValidationServiceProvider::class, 139 Illuminate\Validation\ValidationServiceProvider::class,
140 Illuminate\View\ViewServiceProvider::class, 140 Illuminate\View\ViewServiceProvider::class,
141 + Illuminate\Notifications\NotificationServiceProvider::class,
141 Laravel\Socialite\SocialiteServiceProvider::class, 142 Laravel\Socialite\SocialiteServiceProvider::class,
142 143
143 /** 144 /**
...@@ -156,6 +157,7 @@ return [ ...@@ -156,6 +157,7 @@ return [
156 157
157 BookStack\Providers\AuthServiceProvider::class, 158 BookStack\Providers\AuthServiceProvider::class,
158 BookStack\Providers\AppServiceProvider::class, 159 BookStack\Providers\AppServiceProvider::class,
160 + BookStack\Providers\BroadcastServiceProvider::class,
159 BookStack\Providers\EventServiceProvider::class, 161 BookStack\Providers\EventServiceProvider::class,
160 BookStack\Providers\RouteServiceProvider::class, 162 BookStack\Providers\RouteServiceProvider::class,
161 BookStack\Providers\CustomFacadeProvider::class, 163 BookStack\Providers\CustomFacadeProvider::class,
...@@ -194,6 +196,7 @@ return [ ...@@ -194,6 +196,7 @@ return [
194 'Lang' => Illuminate\Support\Facades\Lang::class, 196 'Lang' => Illuminate\Support\Facades\Lang::class,
195 'Log' => Illuminate\Support\Facades\Log::class, 197 'Log' => Illuminate\Support\Facades\Log::class,
196 'Mail' => Illuminate\Support\Facades\Mail::class, 198 'Mail' => Illuminate\Support\Facades\Mail::class,
199 + 'Notification' => Illuminate\Support\Facades\Notification::class,
197 'Password' => Illuminate\Support\Facades\Password::class, 200 'Password' => Illuminate\Support\Facades\Password::class,
198 'Queue' => Illuminate\Support\Facades\Queue::class, 201 'Queue' => Illuminate\Support\Facades\Queue::class,
199 'Redirect' => Illuminate\Support\Facades\Redirect::class, 202 'Redirect' => Illuminate\Support\Facades\Redirect::class,
......
...@@ -56,7 +56,7 @@ return [ ...@@ -56,7 +56,7 @@ return [
56 56
57 'local' => [ 57 'local' => [
58 'driver' => 'local', 58 'driver' => 'local',
59 - 'root' => public_path(), 59 + 'root' => base_path(),
60 ], 60 ],
61 61
62 'ftp' => [ 62 'ftp' => [
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
6 return [ 6 return [
7 7
8 'app-name' => 'BookStack', 8 'app-name' => 'BookStack',
9 + 'app-name-header' => true,
9 'app-editor' => 'wysiwyg', 10 'app-editor' => 'wysiwyg',
10 'app-color' => '#0288D1', 11 'app-color' => '#0288D1',
11 'app-color-light' => 'rgba(21, 101, 192, 0.15)', 12 'app-color-light' => 'rgba(21, 101, 192, 0.15)',
......
...@@ -129,7 +129,7 @@ class AddRolesAndPermissions extends Migration ...@@ -129,7 +129,7 @@ class AddRolesAndPermissions extends Migration
129 129
130 // Set all current users as admins 130 // Set all current users as admins
131 // (At this point only the initially create user should be an admin) 131 // (At this point only the initially create user should be an admin)
132 - $users = DB::table('users')->get(); 132 + $users = DB::table('users')->get()->all();
133 foreach ($users as $user) { 133 foreach ($users as $user) {
134 DB::table('role_user')->insert([ 134 DB::table('role_user')->insert([
135 'role_id' => $adminId, 135 'role_id' => $adminId,
......
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class RemoveHiddenRoles extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + // Remove the hidden property from roles
17 + Schema::table('roles', function(Blueprint $table) {
18 + $table->dropColumn('hidden');
19 + });
20 +
21 + // Add column to mark system users
22 + Schema::table('users', function(Blueprint $table) {
23 + $table->string('system_name')->nullable()->index();
24 + });
25 +
26 + // Insert our new public system user.
27 + $publicUserId = DB::table('users')->insertGetId([
28 + 'email' => 'guest@example.com',
29 + 'name' => 'Guest',
30 + 'system_name' => 'public',
31 + 'email_confirmed' => true,
32 + 'created_at' => \Carbon\Carbon::now(),
33 + 'updated_at' => \Carbon\Carbon::now(),
34 + ]);
35 +
36 + // Get the public role
37 + $publicRole = DB::table('roles')->where('system_name', '=', 'public')->first();
38 +
39 + // Connect the new public user to the public role
40 + DB::table('role_user')->insert([
41 + 'user_id' => $publicUserId,
42 + 'role_id' => $publicRole->id
43 + ]);
44 + }
45 +
46 + /**
47 + * Reverse the migrations.
48 + *
49 + * @return void
50 + */
51 + public function down()
52 + {
53 + Schema::table('roles', function(Blueprint $table) {
54 + $table->boolean('hidden')->default(false);
55 + $table->index('hidden');
56 + });
57 +
58 + DB::table('users')->where('system_name', '=', 'public')->delete();
59 +
60 + Schema::table('users', function(Blueprint $table) {
61 + $table->dropColumn('system_name');
62 + });
63 +
64 + DB::table('roles')->where('system_name', '=', 'public')->update(['hidden' => true]);
65 + }
66 +}
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class CreateAttachmentsTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::create('attachments', function (Blueprint $table) {
17 + $table->increments('id');
18 + $table->string('name');
19 + $table->string('path');
20 + $table->string('extension', 20);
21 + $table->integer('uploaded_to');
22 +
23 + $table->boolean('external');
24 + $table->integer('order');
25 +
26 + $table->integer('created_by');
27 + $table->integer('updated_by');
28 +
29 + $table->index('uploaded_to');
30 + $table->timestamps();
31 + });
32 +
33 + // Get roles with permissions we need to change
34 + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
35 +
36 + // Create & attach new entity permissions
37 + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
38 + $entity = 'Attachment';
39 + foreach ($ops as $op) {
40 + $permissionId = DB::table('role_permissions')->insertGetId([
41 + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
42 + 'display_name' => $op . ' ' . $entity . 's',
43 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
44 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
45 + ]);
46 + DB::table('permission_role')->insert([
47 + 'role_id' => $adminRoleId,
48 + 'permission_id' => $permissionId
49 + ]);
50 + }
51 +
52 + }
53 +
54 + /**
55 + * Reverse the migrations.
56 + *
57 + * @return void
58 + */
59 + public function down()
60 + {
61 + Schema::dropIfExists('attachments');
62 +
63 + // Create & attach new entity permissions
64 + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
65 + $entity = 'Attachment';
66 + foreach ($ops as $op) {
67 + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
68 + DB::table('role_permissions')->where('name', '=', $permName)->delete();
69 + }
70 + }
71 +}
1 var elixir = require('laravel-elixir'); 1 var elixir = require('laravel-elixir');
2 2
3 -// Custom extensions 3 +elixir(mix => {
4 -var gulp = require('gulp'); 4 + mix.sass('styles.scss');
5 -var Task = elixir.Task; 5 + mix.sass('print-styles.scss');
6 -var fs = require('fs'); 6 + mix.sass('export-styles.scss');
7 - 7 + mix.browserify('global.js', './public/js/common.js');
8 -elixir.extend('queryVersion', function(inputFiles) {
9 - new Task('queryVersion', function() {
10 - var manifestObject = {};
11 - var uidString = Date.now().toString(16).slice(4);
12 - for (var i = 0; i < inputFiles.length; i++) {
13 - var file = inputFiles[i];
14 - manifestObject[file] = file + '?version=' + uidString;
15 - }
16 - var fileContents = JSON.stringify(manifestObject, null, 1);
17 - fs.writeFileSync('public/build/manifest.json', fileContents);
18 - }).watch(['./public/css/*.css', './public/js/*.js']);
19 -});
20 -
21 -elixir(function(mix) {
22 - mix.sass('styles.scss')
23 - .sass('print-styles.scss')
24 - .sass('export-styles.scss')
25 - .browserify('global.js', 'public/js/common.js')
26 - .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
27 }); 8 });
......
1 { 1 {
2 "private": true, 2 "private": true,
3 - "devDependencies": { 3 + "scripts": {
4 - "gulp": "^3.9.0" 4 + "prod": "gulp --production",
5 + "dev": "gulp watch"
5 }, 6 },
6 - "dependencies": { 7 + "devDependencies": {
7 "angular": "^1.5.5", 8 "angular": "^1.5.5",
8 "angular-animate": "^1.5.5", 9 "angular-animate": "^1.5.5",
9 "angular-resource": "^1.5.5", 10 "angular-resource": "^1.5.5",
10 "angular-sanitize": "^1.5.5", 11 "angular-sanitize": "^1.5.5",
11 - "angular-ui-sortable": "^0.14.0", 12 + "angular-ui-sortable": "^0.15.0",
12 - "babel-runtime": "^5.8.29",
13 - "bootstrap-sass": "^3.0.0",
14 "dropzone": "^4.0.1", 13 "dropzone": "^4.0.1",
15 - "laravel-elixir": "^5.0.0", 14 + "gulp": "^3.9.0",
15 + "laravel-elixir": "^6.0.0-11",
16 + "laravel-elixir-browserify-official": "^0.1.3",
16 "marked": "^0.3.5", 17 "marked": "^0.3.5",
17 "moment": "^2.12.0", 18 "moment": "^2.12.0",
18 "zeroclipboard": "^2.2.0" 19 "zeroclipboard": "^2.2.0"
......
1 -suites:
2 - main:
3 - namespace: BookStack
4 - psr4_prefix: BookStack
5 - src_path: app
...\ No newline at end of file ...\ No newline at end of file
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
30 <env name="AUTH_METHOD" value="standard"/> 30 <env name="AUTH_METHOD" value="standard"/>
31 <env name="DISABLE_EXTERNAL_SERVICES" value="true"/> 31 <env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
32 <env name="LDAP_VERSION" value="3"/> 32 <env name="LDAP_VERSION" value="3"/>
33 + <env name="STORAGE_TYPE" value="local"/>
33 <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/> 34 <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
34 <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/> 35 <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
35 <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/> 36 <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
2 2
3 [![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest) 3 [![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest)
4 [![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE) 4 [![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
5 -[![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](https://travis-ci.org/ssddanbrown/BookStack) 5 +[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack)
6 6
7 A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. 7 A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
8 8
9 * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation) 9 * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
10 * [Documentation](https://www.bookstackapp.com/docs) 10 * [Documentation](https://www.bookstackapp.com/docs)
11 -* [Demo Instance](https://demo.bookstackapp.com) *(Login username: `admin@example.com`. Password: `password`)* 11 +* [Demo Instance](https://demo.bookstackapp.com)
12 + * *Username: `admin@example.com`*
13 + * *Password: `password`*
12 * [BookStack Blog](https://www.bookstackapp.com/blog) 14 * [BookStack Blog](https://www.bookstackapp.com/blog)
13 15
14 ## Development & Testing 16 ## Development & Testing
...@@ -29,7 +31,7 @@ php artisan migrate --database=mysql_testing ...@@ -29,7 +31,7 @@ php artisan migrate --database=mysql_testing
29 php artisan db:seed --class=DummyContentSeeder --database=mysql_testing 31 php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
30 ``` 32 ```
31 33
32 -Once done you can run `phpunit` (or `./vendor/bin/phpunit` if `phpunit` is not found) in the application root directory to run all tests. 34 +Once done you can run `phpunit` in the application root directory to run all tests.
33 35
34 ## License 36 ## License
35 37
...@@ -51,3 +53,5 @@ These are the great projects used to help build BookStack: ...@@ -51,3 +53,5 @@ These are the great projects used to help build BookStack:
51 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) 53 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
52 * [Marked](https://github.com/chjj/marked) 54 * [Marked](https://github.com/chjj/marked)
53 * [Moment.js](http://momentjs.com/) 55 * [Moment.js](http://momentjs.com/)
56 +
57 +Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy.
......
1 -<div class="dropzone-container">
2 - <div class="dz-message">Drop files or click here to upload</div>
3 -</div>
...\ No newline at end of file ...\ No newline at end of file
1 -
2 -<div class="image-picker">
3 - <div>
4 - <img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
5 - <img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
6 - </div>
7 - <button class="button" type="button" ng-click="showImageManager()">Select Image</button>
8 - <br>
9 -
10 - <button class="text-button" ng-click="reset()" type="button">Reset</button>
11 - <span ng-show="showRemove" class="sep">|</span>
12 - <button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
13 -
14 - <input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
15 -</div>
...\ No newline at end of file ...\ No newline at end of file
1 -<div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
2 - <input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
3 - <div class="switch-handle"></div>
4 -</div>
...\ No newline at end of file ...\ No newline at end of file
...@@ -2,10 +2,6 @@ ...@@ -2,10 +2,6 @@
2 const DropZone = require('dropzone'); 2 const DropZone = require('dropzone');
3 const markdown = require('marked'); 3 const markdown = require('marked');
4 4
5 -const toggleSwitchTemplate = require('./components/toggle-switch.html');
6 -const imagePickerTemplate = require('./components/image-picker.html');
7 -const dropZoneTemplate = require('./components/drop-zone.html');
8 -
9 module.exports = function (ngApp, events) { 5 module.exports = function (ngApp, events) {
10 6
11 /** 7 /**
...@@ -16,7 +12,12 @@ module.exports = function (ngApp, events) { ...@@ -16,7 +12,12 @@ module.exports = function (ngApp, events) {
16 ngApp.directive('toggleSwitch', function () { 12 ngApp.directive('toggleSwitch', function () {
17 return { 13 return {
18 restrict: 'A', 14 restrict: 'A',
19 - template: toggleSwitchTemplate, 15 + template: `
16 + <div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
17 + <input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
18 + <div class="switch-handle"></div>
19 + </div>
20 + `,
20 scope: true, 21 scope: true,
21 link: function (scope, element, attrs) { 22 link: function (scope, element, attrs) {
22 scope.name = attrs.name; 23 scope.name = attrs.name;
...@@ -33,6 +34,59 @@ module.exports = function (ngApp, events) { ...@@ -33,6 +34,59 @@ module.exports = function (ngApp, events) {
33 }; 34 };
34 }); 35 });
35 36
37 + /**
38 + * Common tab controls using simple jQuery functions.
39 + */
40 + ngApp.directive('tabContainer', function() {
41 + return {
42 + restrict: 'A',
43 + link: function (scope, element, attrs) {
44 + const $content = element.find('[tab-content]');
45 + const $buttons = element.find('[tab-button]');
46 +
47 + if (attrs.tabContainer) {
48 + let initial = attrs.tabContainer;
49 + $buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
50 + $content.hide().filter(`[tab-content="${initial}"]`).show();
51 + } else {
52 + $content.hide().first().show();
53 + $buttons.first().addClass('selected');
54 + }
55 +
56 + $buttons.click(function() {
57 + let clickedTab = $(this);
58 + $buttons.removeClass('selected');
59 + $content.hide();
60 + let name = clickedTab.addClass('selected').attr('tab-button');
61 + $content.filter(`[tab-content="${name}"]`).show();
62 + });
63 + }
64 + };
65 + });
66 +
67 + /**
68 + * Sub form component to allow inner-form sections to act like thier own forms.
69 + */
70 + ngApp.directive('subForm', function() {
71 + return {
72 + restrict: 'A',
73 + link: function (scope, element, attrs) {
74 + element.on('keypress', e => {
75 + if (e.keyCode === 13) {
76 + submitEvent(e);
77 + }
78 + });
79 +
80 + element.find('button[type="submit"]').click(submitEvent);
81 +
82 + function submitEvent(e) {
83 + e.preventDefault()
84 + if (attrs.subForm) scope.$eval(attrs.subForm);
85 + }
86 + }
87 + };
88 + });
89 +
36 90
37 /** 91 /**
38 * Image Picker 92 * Image Picker
...@@ -41,7 +95,22 @@ module.exports = function (ngApp, events) { ...@@ -41,7 +95,22 @@ module.exports = function (ngApp, events) {
41 ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) { 95 ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
42 return { 96 return {
43 restrict: 'E', 97 restrict: 'E',
44 - template: imagePickerTemplate, 98 + template: `
99 + <div class="image-picker">
100 + <div>
101 + <img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
102 + <img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
103 + </div>
104 + <button class="button" type="button" ng-click="showImageManager()">Select Image</button>
105 + <br>
106 +
107 + <button class="text-button" ng-click="reset()" type="button">Reset</button>
108 + <span ng-show="showRemove" class="sep">|</span>
109 + <button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
110 +
111 + <input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
112 + </div>
113 + `,
45 scope: { 114 scope: {
46 name: '@', 115 name: '@',
47 resizeHeight: '@', 116 resizeHeight: '@',
...@@ -108,7 +177,11 @@ module.exports = function (ngApp, events) { ...@@ -108,7 +177,11 @@ module.exports = function (ngApp, events) {
108 ngApp.directive('dropZone', [function () { 177 ngApp.directive('dropZone', [function () {
109 return { 178 return {
110 restrict: 'E', 179 restrict: 'E',
111 - template: dropZoneTemplate, 180 + template: `
181 + <div class="dropzone-container">
182 + <div class="dz-message">Drop files or click here to upload</div>
183 + </div>
184 + `,
112 scope: { 185 scope: {
113 uploadUrl: '@', 186 uploadUrl: '@',
114 eventSuccess: '=', 187 eventSuccess: '=',
...@@ -116,6 +189,7 @@ module.exports = function (ngApp, events) { ...@@ -116,6 +189,7 @@ module.exports = function (ngApp, events) {
116 uploadedTo: '@' 189 uploadedTo: '@'
117 }, 190 },
118 link: function (scope, element, attrs) { 191 link: function (scope, element, attrs) {
192 + if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
119 var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { 193 var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
120 url: scope.uploadUrl, 194 url: scope.uploadUrl,
121 init: function () { 195 init: function () {
...@@ -488,8 +562,8 @@ module.exports = function (ngApp, events) { ...@@ -488,8 +562,8 @@ module.exports = function (ngApp, events) {
488 link: function (scope, elem, attrs) { 562 link: function (scope, elem, attrs) {
489 563
490 // Get common elements 564 // Get common elements
491 - const $buttons = elem.find('[tab-button]'); 565 + const $buttons = elem.find('[toolbox-tab-button]');
492 - const $content = elem.find('[tab-content]'); 566 + const $content = elem.find('[toolbox-tab-content]');
493 const $toggle = elem.find('[toolbox-toggle]'); 567 const $toggle = elem.find('[toolbox-toggle]');
494 568
495 // Handle toolbox toggle click 569 // Handle toolbox toggle click
...@@ -501,17 +575,17 @@ module.exports = function (ngApp, events) { ...@@ -501,17 +575,17 @@ module.exports = function (ngApp, events) {
501 function setActive(tabName, openToolbox) { 575 function setActive(tabName, openToolbox) {
502 $buttons.removeClass('active'); 576 $buttons.removeClass('active');
503 $content.hide(); 577 $content.hide();
504 - $buttons.filter(`[tab-button="${tabName}"]`).addClass('active'); 578 + $buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
505 - $content.filter(`[tab-content="${tabName}"]`).show(); 579 + $content.filter(`[toolbox-tab-content="${tabName}"]`).show();
506 if (openToolbox) elem.addClass('open'); 580 if (openToolbox) elem.addClass('open');
507 } 581 }
508 582
509 // Set the first tab content active on load 583 // Set the first tab content active on load
510 - setActive($content.first().attr('tab-content'), false); 584 + setActive($content.first().attr('toolbox-tab-content'), false);
511 585
512 // Handle tab button click 586 // Handle tab button click
513 $buttons.click(function (e) { 587 $buttons.click(function (e) {
514 - let name = $(this).attr('tab-button'); 588 + let name = $(this).attr('toolbox-tab-button');
515 setActive(name, true); 589 setActive(name, true);
516 }); 590 });
517 } 591 }
...@@ -549,7 +623,7 @@ module.exports = function (ngApp, events) { ...@@ -549,7 +623,7 @@ module.exports = function (ngApp, events) {
549 let val = $input.val(); 623 let val = $input.val();
550 let url = $input.attr('autosuggest'); 624 let url = $input.attr('autosuggest');
551 let type = $input.attr('autosuggest-type'); 625 let type = $input.attr('autosuggest-type');
552 - 626 +
553 // Add name param to request if for a value 627 // Add name param to request if for a value
554 if (type.toLowerCase() === 'value') { 628 if (type.toLowerCase() === 'value') {
555 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); 629 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
...@@ -850,17 +924,3 @@ module.exports = function (ngApp, events) { ...@@ -850,17 +924,3 @@ module.exports = function (ngApp, events) {
850 }; 924 };
851 }]); 925 }]);
852 }; 926 };
853 -
854 -
855 -
856 -
857 -
858 -
859 -
860 -
861 -
862 -
863 -
864 -
865 -
866 -
......
...@@ -38,13 +38,17 @@ class EventManager { ...@@ -38,13 +38,17 @@ class EventManager {
38 this.listeners[eventName].push(callback); 38 this.listeners[eventName].push(callback);
39 return this; 39 return this;
40 } 40 }
41 -}; 41 +}
42 -window.Events = new EventManager();
43 42
43 +window.Events = new EventManager();
44 44
45 -var services = require('./services')(ngApp, window.Events); 45 +// Load in angular specific items
46 -var directives = require('./directives')(ngApp, window.Events); 46 +import Services from './services';
47 -var controllers = require('./controllers')(ngApp, window.Events); 47 +import Directives from './directives';
48 +import Controllers from './controllers';
49 +Services(ngApp, window.Events);
50 +Directives(ngApp, window.Events);
51 +Controllers(ngApp, window.Events);
48 52
49 //Global jQuery Config & Extensions 53 //Global jQuery Config & Extensions
50 54
......
...@@ -6,11 +6,11 @@ ...@@ -6,11 +6,11 @@
6 * @param editor - editor instance 6 * @param editor - editor instance
7 */ 7 */
8 function editorPaste(e, editor) { 8 function editorPaste(e, editor) {
9 - if (!e.clipboardData) return 9 + if (!e.clipboardData) return;
10 let items = e.clipboardData.items; 10 let items = e.clipboardData.items;
11 if (!items) return; 11 if (!items) return;
12 for (let i = 0; i < items.length; i++) { 12 for (let i = 0; i < items.length; i++) {
13 - if (items[i].type.indexOf("image") === -1) return 13 + if (items[i].type.indexOf("image") === -1) return;
14 14
15 let file = items[i].getAsFile(); 15 let file = items[i].getAsFile();
16 let formData = new FormData(); 16 let formData = new FormData();
...@@ -81,9 +81,10 @@ var mceOptions = module.exports = { ...@@ -81,9 +81,10 @@ var mceOptions = module.exports = {
81 toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen", 81 toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
82 content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", 82 content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
83 style_formats: [ 83 style_formats: [
84 - {title: "Header 1", format: "h1"}, 84 + {title: "Header Large", format: "h2"},
85 - {title: "Header 2", format: "h2"}, 85 + {title: "Header Medium", format: "h3"},
86 - {title: "Header 3", format: "h3"}, 86 + {title: "Header Small", format: "h4"},
87 + {title: "Header Tiny", format: "h5"},
87 {title: "Paragraph", format: "p", exact: true, classes: ''}, 88 {title: "Paragraph", format: "p", exact: true, classes: ''},
88 {title: "Blockquote", format: "blockquote"}, 89 {title: "Blockquote", format: "blockquote"},
89 {title: "Code Block", icon: "code", format: "pre"}, 90 {title: "Code Block", icon: "code", format: "pre"},
......
...@@ -43,10 +43,6 @@ ...@@ -43,10 +43,6 @@
43 } 43 }
44 } 44 }
45 45
46 -//body.ie .popup-body {
47 -// min-height: 100%;
48 -//}
49 -
50 .corner-button { 46 .corner-button {
51 position: absolute; 47 position: absolute;
52 top: 0; 48 top: 0;
...@@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { ...@@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
82 min-height: 70vh; 78 min-height: 70vh;
83 } 79 }
84 80
85 -#image-manager .dropzone-container { 81 +.dropzone-container {
86 position: relative; 82 position: relative;
87 border: 3px dashed #DDD; 83 border: 3px dashed #DDD;
88 } 84 }
...@@ -456,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { ...@@ -456,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
456 border-right: 6px solid transparent; 452 border-right: 6px solid transparent;
457 border-bottom: 6px solid $negative; 453 border-bottom: 6px solid $negative;
458 } 454 }
455 +
456 +
457 +[tab-container] .nav-tabs {
458 + text-align: left;
459 + border-bottom: 1px solid #DDD;
460 + margin-bottom: $-m;
461 + .tab-item {
462 + padding: $-s;
463 + color: #666;
464 + &.selected {
465 + border-bottom-width: 3px;
466 + }
467 + }
468 +}
...\ No newline at end of file ...\ No newline at end of file
......
1 .page-list { 1 .page-list {
2 - h3 { 2 + h4 {
3 margin: $-l 0 $-xs 0; 3 margin: $-l 0 $-xs 0;
4 font-size: 1.666em; 4 font-size: 1.666em;
5 } 5 }
...@@ -11,11 +11,13 @@ ...@@ -11,11 +11,13 @@
11 overflow: hidden; 11 overflow: hidden;
12 margin-bottom: $-l; 12 margin-bottom: $-l;
13 } 13 }
14 - h4 { 14 + h5 {
15 display: block; 15 display: block;
16 margin: $-s 0 0 0; 16 margin: $-s 0 0 0;
17 border-left: 5px solid $color-page; 17 border-left: 5px solid $color-page;
18 padding: $-xs 0 $-xs $-m; 18 padding: $-xs 0 $-xs $-m;
19 + font-size: 1.1em;
20 + font-weight: normal;
19 &.draft { 21 &.draft {
20 border-left-color: $color-page-draft; 22 border-left-color: $color-page-draft;
21 } 23 }
...@@ -67,44 +69,39 @@ ...@@ -67,44 +69,39 @@
67 } 69 }
68 } 70 }
69 71
70 -.page-nav-list { 72 +.sidebar-page-nav {
71 $nav-indent: $-s; 73 $nav-indent: $-s;
72 - margin-left: 2px;
73 list-style: none; 74 list-style: none;
75 + margin: $-s 0 $-m 2px;
76 + border-left: 2px dotted #BBB;
74 li { 77 li {
75 - //border-left: 1px solid rgba(0, 0, 0, 0.1); 78 + padding-left: $-s;
76 - padding-left: $-xs;
77 - border-left: 2px solid #888;
78 margin-bottom: 4px; 79 margin-bottom: 4px;
80 + font-size: 0.95em;
79 } 81 }
80 - li a { 82 + .h1 {
81 - color: #555; 83 + margin-left: -2px;
82 } 84 }
83 - .nav-H2 { 85 + .h2 {
86 + margin-left: -2px;
87 + }
88 + .h3 {
84 margin-left: $nav-indent; 89 margin-left: $nav-indent;
85 - font-size: 0.95em;
86 } 90 }
87 - .nav-H3 { 91 + .h4 {
88 margin-left: $nav-indent*2; 92 margin-left: $nav-indent*2;
89 - font-size: 0.90em
90 } 93 }
91 - .nav-H4 { 94 + .h5 {
92 margin-left: $nav-indent*3; 95 margin-left: $nav-indent*3;
93 - font-size: 0.85em
94 } 96 }
95 - .nav-H5 { 97 + .h6 {
96 margin-left: $nav-indent*4; 98 margin-left: $nav-indent*4;
97 - font-size: 0.80em
98 - }
99 - .nav-H6 {
100 - margin-left: $nav-indent*5;
101 - font-size: 0.75em
102 } 99 }
103 } 100 }
104 101
105 // Sidebar list 102 // Sidebar list
106 .book-tree { 103 .book-tree {
107 - padding: $-l 0 0 0; 104 + padding: $-xs 0 0 0;
108 position: relative; 105 position: relative;
109 right: 0; 106 right: 0;
110 top: 0; 107 top: 0;
...@@ -306,10 +303,10 @@ ul.pagination { ...@@ -306,10 +303,10 @@ ul.pagination {
306 } 303 }
307 304
308 .entity-list { 305 .entity-list {
309 - >div { 306 + > div {
310 padding: $-m 0; 307 padding: $-m 0;
311 } 308 }
312 - h3 { 309 + h4 {
313 margin: 0; 310 margin: 0;
314 } 311 }
315 p { 312 p {
...@@ -327,9 +324,10 @@ ul.pagination { ...@@ -327,9 +324,10 @@ ul.pagination {
327 color: $color-page-draft; 324 color: $color-page-draft;
328 } 325 }
329 } 326 }
327 +
330 .entity-list.compact { 328 .entity-list.compact {
331 font-size: 0.6em; 329 font-size: 0.6em;
332 - h3, a { 330 + h4, a {
333 line-height: 1.2; 331 line-height: 1.2;
334 } 332 }
335 p { 333 p {
......
...@@ -71,6 +71,18 @@ ...@@ -71,6 +71,18 @@
71 max-width: 100%; 71 max-width: 100%;
72 height: auto !important; 72 height: auto !important;
73 } 73 }
74 +
75 + // diffs
76 + ins,
77 + del {
78 + text-decoration: none;
79 + }
80 + ins {
81 + background: #dbffdb;
82 + }
83 + del {
84 + background: #FFECEC;
85 + }
74 } 86 }
75 87
76 // Page content pointers 88 // Page content pointers
...@@ -138,7 +150,6 @@ ...@@ -138,7 +150,6 @@
138 background-color: #FFF; 150 background-color: #FFF;
139 border: 1px solid #DDD; 151 border: 1px solid #DDD;
140 right: $-xl*2; 152 right: $-xl*2;
141 - z-index: 99;
142 width: 48px; 153 width: 48px;
143 overflow: hidden; 154 overflow: hidden;
144 align-items: stretch; 155 align-items: stretch;
...@@ -189,7 +200,7 @@ ...@@ -189,7 +200,7 @@
189 color: #444; 200 color: #444;
190 background-color: rgba(0, 0, 0, 0.1); 201 background-color: rgba(0, 0, 0, 0.1);
191 } 202 }
192 - div[tab-content] { 203 + div[toolbox-tab-content] {
193 padding-bottom: 45px; 204 padding-bottom: 45px;
194 display: flex; 205 display: flex;
195 flex: 1; 206 flex: 1;
...@@ -197,7 +208,7 @@ ...@@ -197,7 +208,7 @@
197 min-height: 0px; 208 min-height: 0px;
198 overflow-y: scroll; 209 overflow-y: scroll;
199 } 210 }
200 - div[tab-content] .padded { 211 + div[toolbox-tab-content] .padded {
201 flex: 1; 212 flex: 1;
202 padding-top: 0; 213 padding-top: 0;
203 } 214 }
...@@ -216,21 +227,6 @@ ...@@ -216,21 +227,6 @@
216 padding-top: $-s; 227 padding-top: $-s;
217 position: relative; 228 position: relative;
218 } 229 }
219 - button.pos {
220 - position: absolute;
221 - bottom: 0;
222 - display: block;
223 - width: 100%;
224 - padding: $-s;
225 - height: 45px;
226 - border: 0;
227 - margin: 0;
228 - box-shadow: none;
229 - border-radius: 0;
230 - &:hover{
231 - box-shadow: none;
232 - }
233 - }
234 .handle { 230 .handle {
235 user-select: none; 231 user-select: none;
236 cursor: move; 232 cursor: move;
...@@ -242,9 +238,12 @@ ...@@ -242,9 +238,12 @@
242 flex-direction: column; 238 flex-direction: column;
243 overflow-y: scroll; 239 overflow-y: scroll;
244 } 240 }
241 + table td, table th {
242 + overflow: visible;
243 + }
245 } 244 }
246 245
247 -[tab-content] { 246 +[toolbox-tab-content] {
248 display: none; 247 display: none;
249 } 248 }
250 249
......
...@@ -51,4 +51,14 @@ table.list-table { ...@@ -51,4 +51,14 @@ table.list-table {
51 vertical-align: middle; 51 vertical-align: middle;
52 padding: $-xs; 52 padding: $-xs;
53 } 53 }
54 +}
55 +
56 +table.file-table {
57 + @extend .no-style;
58 + td {
59 + padding: $-xs;
60 + }
61 + .ui-sortable-helper {
62 + display: table;
63 + }
54 } 64 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -15,31 +15,41 @@ h2 { ...@@ -15,31 +15,41 @@ h2 {
15 margin-bottom: 0.43137255em; 15 margin-bottom: 0.43137255em;
16 } 16 }
17 h3 { 17 h3 {
18 - font-size: 1.75em; 18 + font-size: 2.333em;
19 line-height: 1.571428572em; 19 line-height: 1.571428572em;
20 margin-top: 0.78571429em; 20 margin-top: 0.78571429em;
21 margin-bottom: 0.43137255em; 21 margin-bottom: 0.43137255em;
22 } 22 }
23 h4 { 23 h4 {
24 - font-size: 1em; 24 + font-size: 1.666em;
25 line-height: 1.375em; 25 line-height: 1.375em;
26 margin-top: 0.78571429em; 26 margin-top: 0.78571429em;
27 margin-bottom: 0.43137255em; 27 margin-bottom: 0.43137255em;
28 } 28 }
29 29
30 -h1, h2, h3, h4 { 30 +h1, h2, h3, h4, h5, h6 {
31 font-weight: 400; 31 font-weight: 400;
32 position: relative; 32 position: relative;
33 display: block; 33 display: block;
34 color: #555; 34 color: #555;
35 .subheader { 35 .subheader {
36 - //display: block;
37 font-size: 0.5em; 36 font-size: 0.5em;
38 line-height: 1em; 37 line-height: 1em;
39 color: lighten($text-dark, 32%); 38 color: lighten($text-dark, 32%);
40 } 39 }
41 } 40 }
42 41
42 +h5 {
43 + font-size: 1.4em;
44 +}
45 +
46 +h5, h6 {
47 + font-weight: 500;
48 + line-height: 1.2em;
49 + margin-top: 0.78571429em;
50 + margin-bottom: 0.66em;
51 +}
52 +
43 /* 53 /*
44 * Link styling 54 * Link styling
45 */ 55 */
...@@ -183,7 +193,7 @@ p.neg, p .neg, span.neg, .text-neg { ...@@ -183,7 +193,7 @@ p.neg, p .neg, span.neg, .text-neg {
183 p.muted, p .muted, span.muted, .text-muted { 193 p.muted, p .muted, span.muted, .text-muted {
184 color: lighten($text-dark, 26%); 194 color: lighten($text-dark, 26%);
185 &.small, .small { 195 &.small, .small {
186 - color: lighten($text-dark, 42%); 196 + color: lighten($text-dark, 32%);
187 } 197 }
188 } 198 }
189 199
......
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Activity text strings.
7 + * Is used for all the text within activity logs & notifications.
8 + */
9 +
10 + // Pages
11 + 'page_create' => 'Seite erstellt',
12 + 'page_create_notification' => 'Seite erfolgreich erstellt',
13 + 'page_update' => 'Seite aktualisiert',
14 + 'page_update_notification' => 'Seite erfolgreich aktualisiert',
15 + 'page_delete' => 'Seite gel&ouml;scht',
16 + 'page_delete_notification' => 'Seite erfolgreich gel&ouml;scht',
17 + 'page_restore' => 'Seite wiederhergstellt',
18 + 'page_restore_notification' => 'Seite erfolgreich wiederhergstellt',
19 + 'page_move' => 'Seite verschoben',
20 +
21 + // Chapters
22 + 'chapter_create' => 'Kapitel erstellt',
23 + 'chapter_create_notification' => 'Kapitel erfolgreich erstellt',
24 + 'chapter_update' => 'Kapitel aktualisiert',
25 + 'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',
26 + 'chapter_delete' => 'Kapitel gel&ouml;scht',
27 + 'chapter_delete_notification' => 'Kapitel erfolgreich gel&ouml;scht',
28 + 'chapter_move' => 'Kapitel verschoben',
29 +
30 + // Books
31 + 'book_create' => 'Buch erstellt',
32 + 'book_create_notification' => 'Buch erfolgreich erstellt',
33 + 'book_update' => 'Buch aktualisiert',
34 + 'book_update_notification' => 'Buch erfolgreich aktualisiert',
35 + 'book_delete' => 'Buch gel&ouml;scht',
36 + 'book_delete_notification' => 'Buch erfolgreich gel&ouml;scht',
37 + 'book_sort' => 'Buch sortiert',
38 + 'book_sort_notification' => 'Buch erfolgreich neu sortiert',
39 +
40 +];
1 +<?php
2 +return [
3 + /*
4 + |--------------------------------------------------------------------------
5 + | Authentication Language Lines
6 + |--------------------------------------------------------------------------
7 + |
8 + | The following language lines are used during authentication for various
9 + | messages that we need to display to the user. You are free to modify
10 + | these language lines according to your application's requirements.
11 + |
12 + */
13 + 'failed' => 'Dies sind keine g&uuml;ltigen Anmeldedaten.',
14 + 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.',
15 +
16 + /**
17 + * Email Confirmation Text
18 + */
19 + 'email_confirm_subject' => 'Best&auml;tigen sie ihre E-Mail Adresse bei :appName',
20 + 'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!',
21 + 'email_confirm_text' => 'Bitte best&auml;tigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:',
22 + 'email_confirm_action' => 'E-Mail Adresse best&auml;tigen',
23 + 'email_confirm_send_error' => 'Best&auml;tigungs-E-Mail ben&ouml;tigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.',
24 + 'email_confirm_success' => 'Ihre E-Mail Adresse wurde best&auml;tigt!',
25 + 'email_confirm_resent' => 'Best&auml;tigungs-E-Mail wurde erneut versendet, bitte &uuml;berpr&uuml;fen sie ihren Posteingang.',
26 +];
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Error text strings.
7 + */
8 +
9 + // Pages
10 + 'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
11 + 'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.'
12 +];
1 +<?php
2 +
3 +return [
4 +
5 + /*
6 + |--------------------------------------------------------------------------
7 + | Pagination Language Lines
8 + |--------------------------------------------------------------------------
9 + |
10 + | The following language lines are used by the paginator library to build
11 + | the simple pagination links. You are free to change them to anything
12 + | you want to customize your views to better match your application.
13 + |
14 + */
15 +
16 + 'previous' => '&laquo; Vorherige',
17 + 'next' => 'N&auml;chste &raquo;',
18 +
19 +];
1 +<?php
2 +
3 +return [
4 +
5 + /*
6 + |--------------------------------------------------------------------------
7 + | Password Reminder Language Lines
8 + |--------------------------------------------------------------------------
9 + |
10 + | The following language lines are the default lines which match reasons
11 + | that are given by the password broker for a password update attempt
12 + | has failed, such as for an invalid token or invalid new password.
13 + |
14 + */
15 +
16 + 'password' => 'Pass&ouml;rter m&uuml;ssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.',
17 + 'user' => "Wir k&ouml;nnen keinen Benutzer mit dieser E-Mail Adresse finden.",
18 + 'token' => 'Dieser Passwort-Reset-Token ist ung&uuml;ltig.',
19 + 'sent' => 'Wir haben ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!',
20 + 'reset' => 'Ihr Passwort wurde zur&uuml;ckgesetzt!',
21 +
22 +];
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Settings text strings
7 + * Contains all text strings used in the general settings sections of BookStack
8 + * including users and roles.
9 + */
10 +
11 + 'settings' => 'Einstellungen',
12 + 'settings_save' => 'Einstellungen speichern',
13 +
14 + 'app_settings' => 'Anwendungseinstellungen',
15 + 'app_name' => 'Anwendungsname',
16 + 'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
17 + 'app_name_header' => 'Anwendungsname im Header anzeigen?',
18 + 'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?',
19 + 'app_secure_images' => 'Erh&oml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
20 + 'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
21 + 'app_editor' => 'Seiteneditor',
22 + 'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
23 + 'app_custom_html' => 'Benutzerdefinierter HTML <head> Inhalt',
24 + 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugef&uuml;gt wird, wird am Ende der <head> Sektion jeder Seite eingef&uuml;gt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics Code hinzuzuf&uuml;gen.',
25 + 'app_logo' => 'Anwendungslogo',
26 + 'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein. <br>Gr&ouml;&szlig;ere Bilder werden verkleinert.',
27 + 'app_primary_color' => 'Prim&auml;re Anwendungsfarbe',
28 + 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>Leer lassen des Feldes setzt auf die Standard-Anwendungsfarbe zur&uuml;ck.',
29 +
30 + 'reg_settings' => 'Registrierungseinstellungen',
31 + 'reg_allow' => 'Registrierung erlauben?',
32 + 'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
33 + 'reg_confirm_email' => 'Best&auml;tigung per E-Mail erforderlich?',
34 + 'reg_confirm_email_desc' => 'Falls die Einschr&auml;nkung f&uumlr; Domains genutzt wird, ist die Best&auml;tigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
35 + 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschr&auml;nken',
36 + 'reg_confirm_restrict_domain_desc' => 'F&uuml;gen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschr&auml;nkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu best&auml;tigen, bevor sie diese Anwendung nutzen k&ouml;nnen. <br> Hinweis: Benutzer k&ouml;nnen ihre E-Mail Adresse nach erfolgreicher Registrierung &auml;ndern.',
37 + 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschr&auml;nkung gesetzt',
38 +
39 +];
1 +<?php
2 +
3 +return [
4 +
5 + /*
6 + |--------------------------------------------------------------------------
7 + | Validation Language Lines
8 + |--------------------------------------------------------------------------
9 + |
10 + | following language lines contain default error messages used by
11 + | validator class. Some of these rules have multiple versions such
12 + | as size rules. Feel free to tweak each of these messages here.
13 + |
14 + */
15 +
16 + 'accepted' => ':attribute muss akzeptiert werden.',
17 + 'active_url' => ':attribute ist keine valide URL.',
18 + 'after' => ':attribute muss ein Datum nach :date sein.',
19 + 'alpha' => ':attribute kann nur Buchstaben enthalten.',
20 + 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',
21 + 'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.',
22 + 'array' => ':attribute muss eine Array sein.',
23 + 'before' => ':attribute muss ein Datum vor :date sein.',
24 + 'between' => [
25 + 'numeric' => ':attribute muss zwischen :min und :max liegen.',
26 + 'file' => ':attribute muss zwischen :min und :max Kilobytes gro&szlig; sein.',
27 + 'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.',
28 + 'array' => ':attribute muss zwischen :min und :max Elemente enthalten.',
29 + ],
30 + 'boolean' => ':attribute Feld muss wahr oder falsch sein.',
31 + 'confirmed' => ':attribute Best&auml;tigung stimmt nicht &uuml;berein.',
32 + 'date' => ':attribute ist kein valides Datum.',
33 + 'date_format' => ':attribute entspricht nicht dem Format :format.',
34 + 'different' => ':attribute und :other m&uuml;ssen unterschiedlich sein.',
35 + 'digits' => ':attribute muss :digits Stellen haben.',
36 + 'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.',
37 + 'email' => ':attribute muss eine valide E-Mail Adresse sein.',
38 + 'filled' => ':attribute Feld ist erforderlich.',
39 + 'exists' => 'Markiertes :attribute ist ung&uuml;ltig.',
40 + 'image' => ':attribute muss ein Bild sein.',
41 + 'in' => 'Markiertes :attribute ist ung&uuml;ltig.',
42 + 'integer' => ':attribute muss eine Zahl sein.',
43 + 'ip' => ':attribute muss eine valide IP-Adresse sein.',
44 + 'max' => [
45 + 'numeric' => ':attribute darf nicht gr&ouml;&szlig;er als :max sein.',
46 + 'file' => ':attribute darf nicht gr&ouml;&szlig;er als :max Kilobyte sein.',
47 + 'string' => ':attribute darf nicht l&auml;nger als :max Zeichen sein.',
48 + 'array' => ':attribute darf nicht mehr als :max Elemente enthalten.',
49 + ],
50 + 'mimes' => ':attribute muss eine Datei vom Typ: :values sein.',
51 + 'min' => [
52 + 'numeric' => ':attribute muss mindestens :min. sein',
53 + 'file' => ':attribute muss mindestens :min Kilobyte gro&szlig; sein.',
54 + 'string' => ':attribute muss mindestens :min Zeichen lang sein.',
55 + 'array' => ':attribute muss mindesten :min Elemente enthalten.',
56 + ],
57 + 'not_in' => 'Markiertes :attribute ist ung&uuml;ltig.',
58 + 'numeric' => ':attribute muss eine Zahl sein.',
59 + 'regex' => ':attribute Format ist ung&uuml;ltig.',
60 + 'required' => ':attribute Feld ist erforderlich.',
61 + 'required_if' => ':attribute Feld ist erforderlich, wenn :other :value ist.',
62 + 'required_with' => ':attribute Feld ist erforderlich, wenn :values vorhanden ist.',
63 + 'required_with_all' => ':attribute Feld ist erforderlich, wenn :values vorhanden sind.',
64 + 'required_without' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden ist.',
65 + 'required_without_all' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden sind.',
66 + 'same' => ':attribute und :other muss &uuml;bereinstimmen.',
67 + 'size' => [
68 + 'numeric' => ':attribute muss :size sein.',
69 + 'file' => ':attribute muss :size Kilobytes gro&szlig; sein.',
70 + 'string' => ':attribute muss :size Zeichen lang sein.',
71 + 'array' => ':attribute muss :size Elemente enthalten.',
72 + ],
73 + 'string' => ':attribute muss eine Zeichenkette sein.',
74 + 'timezone' => ':attribute muss eine valide zeitzone sein.',
75 + 'unique' => ':attribute wird bereits verwendet.',
76 + 'url' => ':attribute ist kein valides Format.',
77 +
78 + /*
79 + |--------------------------------------------------------------------------
80 + | Custom Validation Language Lines
81 + |--------------------------------------------------------------------------
82 + |
83 + | Here you may specify custom validation messages for attributes using the
84 + | convention "attribute.rule" to name lines. This makes it quick to
85 + | specify a specific custom language line for a given attribute rule.
86 + |
87 + */
88 +
89 + 'custom' => [
90 + 'attribute-name' => [
91 + 'rule-name' => 'custom-message',
92 + ],
93 + ],
94 +
95 + /*
96 + |--------------------------------------------------------------------------
97 + | Custom Validation Attributes
98 + |--------------------------------------------------------------------------
99 + |
100 + | following language lines are used to swap attribute place-holders
101 + | with something more reader friendly such as E-Mail Address instead
102 + | of "email". This simply helps us make messages a little cleaner.
103 + |
104 + */
105 +
106 + 'attributes' => [],
107 +
108 +];
1 +<?php
2 +return [
3 + /*
4 + |--------------------------------------------------------------------------
5 + | Authentication Language Lines
6 + |--------------------------------------------------------------------------
7 + |
8 + | The following language lines are used during authentication for various
9 + | messages that we need to display to the user. You are free to modify
10 + | these language lines according to your application's requirements.
11 + |
12 + */
13 + 'failed' => 'These credentials do not match our records.',
14 + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
15 +
16 + /**
17 + * Email Confirmation Text
18 + */
19 + 'email_confirm_subject' => 'Confirm your email on :appName',
20 + 'email_confirm_greeting' => 'Thanks for joining :appName!',
21 + 'email_confirm_text' => 'Please confirm your email address by clicking the button below:',
22 + 'email_confirm_action' => 'Confirm Email',
23 + 'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
24 + 'email_confirm_success' => 'Your email has been confirmed!',
25 + 'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
26 +];
...\ No newline at end of file ...\ No newline at end of file
1 +<?php
2 +
3 +return [
4 +
5 + /**
6 + * Settings text strings
7 + * Contains all text strings used in the general settings sections of BookStack
8 + * including users and roles.
9 + */
10 +
11 + 'settings' => 'Settings',
12 + 'settings_save' => 'Save Settings',
13 +
14 + 'app_settings' => 'App Settings',
15 + 'app_name' => 'Application name',
16 + 'app_name_desc' => 'This name is shown in the header and any emails.',
17 + 'app_name_header' => 'Show Application name in header?',
18 + 'app_public_viewing' => 'Allow public viewing?',
19 + 'app_secure_images' => 'Enable higher security image uploads?',
20 + 'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
21 + 'app_editor' => 'Page editor',
22 + 'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
23 + 'app_custom_html' => 'Custom HTML head content',
24 + 'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
25 + 'app_logo' => 'Application logo',
26 + 'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
27 + 'app_primary_color' => 'Application primary color',
28 + 'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
29 +
30 + 'reg_settings' => 'Registration Settings',
31 + 'reg_allow' => 'Allow registration?',
32 + 'reg_default_role' => 'Default user role after registration',
33 + 'reg_confirm_email' => 'Require email confirmation?',
34 + 'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.',
35 + 'reg_confirm_restrict_domain' => 'Restrict registration to domain',
36 + 'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
37 + 'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
38 +
39 +];
...\ No newline at end of file ...\ No newline at end of file
...@@ -39,7 +39,9 @@ ...@@ -39,7 +39,9 @@
39 @if(setting('app-logo', '') !== 'none') 39 @if(setting('app-logo', '') !== 'none')
40 <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo"> 40 <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
41 @endif 41 @endif
42 - <span class="logo-text">{{ setting('app-name') }}</span> 42 + @if (setting('app-name-header'))
43 + <span class="logo-text">{{ setting('app-name') }}</span>
44 + @endif
43 </a> 45 </a>
44 </div> 46 </div>
45 <div class="col-lg-4 col-sm-3 text-center"> 47 <div class="col-lg-4 col-sm-3 text-center">
......
1 <div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}"> 1 <div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
2 - <h3 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h3> 2 + <h4 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h4>
3 @if(isset($book->searchSnippet)) 3 @if(isset($book->searchSnippet))
4 <p class="text-muted">{!! $book->searchSnippet !!}</p> 4 <p class="text-muted">{!! $book->searchSnippet !!}</p>
5 @else 5 @else
......
1 <div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}"> 1 <div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
2 - <h3> 2 + <h4>
3 @if (isset($showPath) && $showPath) 3 @if (isset($showPath) && $showPath)
4 <a href="{{ $chapter->book->getUrl() }}" class="text-book"> 4 <a href="{{ $chapter->book->getUrl() }}" class="text-book">
5 <i class="zmdi zmdi-book"></i>{{ $chapter->book->name }} 5 <i class="zmdi zmdi-book"></i>{{ $chapter->book->name }}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
9 <a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link"> 9 <a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
10 <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span> 10 <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
11 </a> 11 </a>
12 - </h3> 12 + </h4>
13 @if(isset($chapter->searchSnippet)) 13 @if(isset($chapter->searchSnippet))
14 <p class="text-muted">{!! $chapter->searchSnippet !!}</p> 14 <p class="text-muted">{!! $chapter->searchSnippet !!}</p>
15 @else 15 @else
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
20 <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p> 20 <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
21 <div class="inset-list"> 21 <div class="inset-list">
22 @foreach($chapter->pages as $page) 22 @foreach($chapter->pages as $page)
23 - <h4 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h4> 23 + <h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>
24 @endforeach 24 @endforeach
25 </div> 25 </div>
26 @endif 26 @endif
......
...@@ -25,14 +25,14 @@ ...@@ -25,14 +25,14 @@
25 <div class="col-sm-4"> 25 <div class="col-sm-4">
26 <div id="recent-drafts"> 26 <div id="recent-drafts">
27 @if(count($draftPages) > 0) 27 @if(count($draftPages) > 0)
28 - <h3>My Recent Drafts</h3> 28 + <h4>My Recent Drafts</h4>
29 @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact']) 29 @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
30 @endif 30 @endif
31 </div> 31 </div>
32 @if($signedIn) 32 @if($signedIn)
33 - <h3>My Recently Viewed</h3> 33 + <h4>My Recently Viewed</h4>
34 @else 34 @else
35 - <h3>Recent Books</h3> 35 + <h4>Recent Books</h4>
36 @endif 36 @endif
37 @include('partials/entity-list', [ 37 @include('partials/entity-list', [
38 'entities' => $recents, 38 'entities' => $recents,
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
42 </div> 42 </div>
43 43
44 <div class="col-sm-4"> 44 <div class="col-sm-4">
45 - <h3><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h3> 45 + <h4><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h4>
46 <div id="recently-created-pages"> 46 <div id="recently-created-pages">
47 @include('partials/entity-list', [ 47 @include('partials/entity-list', [
48 'entities' => $recentlyCreatedPages, 48 'entities' => $recentlyCreatedPages,
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
51 ]) 51 ])
52 </div> 52 </div>
53 53
54 - <h3><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h3> 54 + <h4><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h4>
55 <div id="recently-updated-pages"> 55 <div id="recently-updated-pages">
56 @include('partials/entity-list', [ 56 @include('partials/entity-list', [
57 'entities' => $recentlyUpdatedPages, 57 'entities' => $recentlyUpdatedPages,
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
62 </div> 62 </div>
63 63
64 <div class="col-sm-4" id="recent-activity"> 64 <div class="col-sm-4" id="recent-activity">
65 - <h3>Recent Activity</h3> 65 + <h4>Recent Activity</h4>
66 @include('partials/activity-list', ['activity' => $activity]) 66 @include('partials/activity-list', ['activity' => $activity])
67 </div> 67 </div>
68 68
......
...@@ -23,10 +23,4 @@ ...@@ -23,10 +23,4 @@
23 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) 23 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
24 @include('partials/entity-selector-popup') 24 @include('partials/entity-selector-popup')
25 25
26 - <script>
27 - (function() {
28 -
29 - })();
30 - </script>
31 -
32 @stop 26 @stop
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -3,10 +3,13 @@ ...@@ -3,10 +3,13 @@
3 3
4 <div class="tabs primary-background-light"> 4 <div class="tabs primary-background-light">
5 <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span> 5 <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
6 - <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span> 6 + <span toolbox-tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
7 + @if(userCan('attachment-create-all'))
8 + <span toolbox-tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
9 + @endif
7 </div> 10 </div>
8 11
9 - <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}"> 12 + <div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
10 <h4>Page Tags</h4> 13 <h4>Page Tags</h4>
11 <div class="padded tags"> 14 <div class="padded tags">
12 <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> 15 <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 @@ ...@@ -34,4 +37,98 @@
34 </div> 37 </div>
35 </div> 38 </div>
36 39
40 + @if(userCan('attachment-create-all'))
41 + <div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
42 + <h4>Attachments</h4>
43 + <div class="padded files">
44 +
45 + <div id="file-list" ng-show="!editFile">
46 + <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>
47 +
48 + <div tab-container>
49 + <div class="nav-tabs">
50 + <div tab-button="list" class="tab-item">Attached Items</div>
51 + <div tab-button="file" class="tab-item">Upload File</div>
52 + <div tab-button="link" class="tab-item">Attach Link</div>
53 + </div>
54 + <div tab-content="list">
55 + <table class="file-table" style="width: 100%;">
56 + <tbody ui-sortable="sortOptions" ng-model="files" >
57 + <tr ng-repeat="file in files track by $index">
58 + <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
59 + <td>
60 + <a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a>
61 + <div ng-if="file.deleting">
62 + <span class="neg small">Click delete again to confirm you want to delete this attachment.</span>
63 + <br>
64 + <span class="text-primary small" ng-click="file.deleting=false;">Cancel</span>
65 + </div>
66 + </td>
67 + <td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
68 + <td width="5"></td>
69 + <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
70 + </tr>
71 + </tbody>
72 + </table>
73 + <p class="small muted" ng-if="files.length == 0">
74 + No files have been uploaded.
75 + </p>
76 + </div>
77 + <div tab-content="file">
78 + <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
79 + </div>
80 + <div tab-content="link" sub-form="attachLinkSubmit(file)">
81 + <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>
82 + <div class="form-group">
83 + <label for="attachment-via-link">Link Name</label>
84 + <input type="text" placeholder="Link name" ng-model="file.name">
85 + <p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
86 + </div>
87 + <div class="form-group">
88 + <label for="attachment-via-link">Link to file</label>
89 + <input type="text" placeholder="Url of site or file" ng-model="file.link">
90 + <p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
91 + </div>
92 + <button type="submit" class="button pos">Attach</button>
93 +
94 + </div>
95 + </div>
96 +
97 + </div>
98 +
99 + <div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)">
100 + <h5>Edit File</h5>
101 +
102 + <div class="form-group">
103 + <label for="attachment-name-edit">File Name</label>
104 + <input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
105 + <p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p>
106 + </div>
107 +
108 + <div tab-container="@{{ editFile.external ? 'link' : 'file' }}">
109 + <div class="nav-tabs">
110 + <div tab-button="file" class="tab-item">Upload File</div>
111 + <div tab-button="link" class="tab-item">Set Link</div>
112 + </div>
113 + <div tab-content="file">
114 + <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
115 + <br>
116 + </div>
117 + <div tab-content="link">
118 + <div class="form-group">
119 + <label for="attachment-link-edit">Link to file</label>
120 + <input type="text" id="attachment-link-edit" placeholder="Attachment link" ng-model="editFile.link">
121 + <p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
122 + </div>
123 + </div>
124 + </div>
125 +
126 + <button type="button" class="button" ng-click="cancelEdit()">Back</button>
127 + <button type="submit" class="button pos">Save</button>
128 + </div>
129 +
130 + </div>
131 + </div>
132 + @endif
133 +
37 </div> 134 </div>
...\ No newline at end of file ...\ No newline at end of file
......
1 1
2 -<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 }}"> 2 +<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 }}">
3 3
4 {{ csrf_field() }} 4 {{ csrf_field() }}
5 +
6 + {{--Header Bar--}}
5 <div class="faded-small toolbar"> 7 <div class="faded-small toolbar">
6 <div class="container"> 8 <div class="container">
7 <div class="row"> 9 <div class="row">
...@@ -13,7 +15,7 @@ ...@@ -13,7 +15,7 @@
13 </div> 15 </div>
14 <div class="col-sm-4 faded text-center"> 16 <div class="col-sm-4 faded text-center">
15 17
16 - <div dropdown class="dropdown-container draft-display"> 18 + <div ng-show="draftsEnabled" dropdown class="dropdown-container draft-display">
17 <a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a> 19 <a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a>
18 <i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i> 20 <i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
19 <ul> 21 <ul>
...@@ -48,13 +50,17 @@ ...@@ -48,13 +50,17 @@
48 </div> 50 </div>
49 </div> 51 </div>
50 52
53 + {{--Title input--}}
51 <div class="title-input page-title clearfix" ng-non-bindable> 54 <div class="title-input page-title clearfix" ng-non-bindable>
52 <div class="input"> 55 <div class="input">
53 @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title']) 56 @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
54 </div> 57 </div>
55 </div> 58 </div>
56 59
60 + {{--Editors--}}
57 <div class="edit-area flex-fill flex"> 61 <div class="edit-area flex-fill flex">
62 +
63 + {{--WYSIWYG Editor--}}
58 @if(setting('app-editor') === 'wysiwyg') 64 @if(setting('app-editor') === 'wysiwyg')
59 <div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex"> 65 <div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
60 <textarea id="html-editor" name="html" rows="5" ng-non-bindable 66 <textarea id="html-editor" name="html" rows="5" ng-non-bindable
...@@ -66,6 +72,7 @@ ...@@ -66,6 +72,7 @@
66 @endif 72 @endif
67 @endif 73 @endif
68 74
75 + {{--Markdown Editor--}}
69 @if(setting('app-editor') === 'markdown') 76 @if(setting('app-editor') === 'markdown')
70 <div id="markdown-editor" markdown-editor class="flex-fill flex"> 77 <div id="markdown-editor" markdown-editor class="flex-fill flex">
71 78
...@@ -102,7 +109,7 @@ ...@@ -102,7 +109,7 @@
102 @if($errors->has('markdown')) 109 @if($errors->has('markdown'))
103 <div class="text-neg text-small">{{ $errors->first('markdown') }}</div> 110 <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
104 @endif 111 @endif
105 -
106 @endif 112 @endif
113 +
107 </div> 114 </div>
108 </div> 115 </div>
...\ No newline at end of file ...\ No newline at end of file
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="container small" ng-non-bindable>
6 + <h1>Create Page</h1>
7 + <form action="{{ $parent->getUrl('/page/create/guest') }}" method="POST">
8 +
9 + {!! csrf_field() !!}
10 +
11 + <div class="form-group title-input">
12 + <label for="name">Page Name</label>
13 + @include('form/text', ['name' => 'name'])
14 + </div>
15 +
16 + <div class="form-group">
17 + <a href="{{ $parent->getUrl() }}" class="button muted">Cancel</a>
18 + <button type="submit" class="button pos">Continue</button>
19 + </div>
20 +
21 + </form>
22 + </div>
23 +
24 +
25 +@stop
...\ No newline at end of file ...\ No newline at end of file
1 <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}"> 1 <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
2 - <h3> 2 + <h4>
3 <a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a> 3 <a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
4 - </h3> 4 + </h4>
5 5
6 @if(isset($page->searchSnippet)) 6 @if(isset($page->searchSnippet))
7 <p class="text-muted">{!! $page->searchSnippet !!}</p> 7 <p class="text-muted">{!! $page->searchSnippet !!}</p>
......
...@@ -24,5 +24,9 @@ ...@@ -24,5 +24,9 @@
24 24
25 <div style="clear:left;"></div> 25 <div style="clear:left;"></div>
26 26
27 - {!! $page->html !!} 27 + @if (isset($diff) && $diff)
28 + {!! $diff !!}
29 + @else
30 + {!! $page->html !!}
31 + @endif
28 </div> 32 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -32,11 +32,11 @@ ...@@ -32,11 +32,11 @@
32 32
33 <table class="table"> 33 <table class="table">
34 <tr> 34 <tr>
35 - <th width="25%">Name</th> 35 + <th width="23%">Name</th>
36 - <th colspan="2" width="10%">Created By</th> 36 + <th colspan="2" width="8%">Created By</th>
37 <th width="15%">Revision Date</th> 37 <th width="15%">Revision Date</th>
38 <th width="25%">Changelog</th> 38 <th width="25%">Changelog</th>
39 - <th width="15%">Actions</th> 39 + <th width="20%">Actions</th>
40 </tr> 40 </tr>
41 @foreach($page->revisions as $index => $revision) 41 @foreach($page->revisions as $index => $revision)
42 <tr> 42 <tr>
...@@ -49,15 +49,18 @@ ...@@ -49,15 +49,18 @@
49 <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td> 49 <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
50 <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td> 50 <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
51 <td>{{ $revision->summary }}</td> 51 <td>{{ $revision->summary }}</td>
52 - @if ($index !== 0) 52 + <td>
53 - <td> 53 + <a href="{{ $revision->getUrl('changes') }}" target="_blank">Changes</a>
54 + <span class="text-muted">&nbsp;|&nbsp;</span>
55 +
56 + @if ($index === 0)
57 + <a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a>
58 + @else
54 <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a> 59 <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
55 <span class="text-muted">&nbsp;|&nbsp;</span> 60 <span class="text-muted">&nbsp;|&nbsp;</span>
56 - <a href="{{ $revision->getUrl() }}/restore">Restore</a> 61 + <a href="{{ $revision->getUrl('restore') }}" target="_blank">Restore</a>
57 - </td> 62 + @endif
58 - @else 63 + </td>
59 - <td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
60 - @endif
61 </tr> 64 </tr>
62 @endforeach 65 @endforeach
63 </table> 66 </table>
......
...@@ -114,7 +114,8 @@ ...@@ -114,7 +114,8 @@
114 @endif 114 @endif
115 </div> 115 </div>
116 @endif 116 @endif
117 - @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) 117 +
118 + @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav])
118 </div> 119 </div>
119 120
120 </div> 121 </div>
......
This diff is collapsed. Click to expand it.