Dan Brown

Merge branch 'master' into release

Showing 127 changed files with 3327 additions and 979 deletions
...@@ -11,3 +11,5 @@ Homestead.yaml ...@@ -11,3 +11,5 @@ Homestead.yaml
11 /storage/images 11 /storage/images
12 _ide_helper.php 12 _ide_helper.php
13 /storage/debugbar 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
1 -<?php namespace BookStack\Http\Controllers\Auth; 1 +<?php
2 2
3 -use BookStack\Exceptions\AuthException; 3 +namespace BookStack\Http\Controllers\Auth;
4 -use Illuminate\Contracts\Auth\Authenticatable; 4 +
5 -use Illuminate\Http\Request; 5 +use BookStack\Exceptions\ConfirmationEmailException;
6 -use BookStack\Exceptions\SocialSignInException;
7 use BookStack\Exceptions\UserRegistrationException; 6 use BookStack\Exceptions\UserRegistrationException;
8 use BookStack\Repos\UserRepo; 7 use BookStack\Repos\UserRepo;
9 use BookStack\Services\EmailConfirmationService; 8 use BookStack\Services\EmailConfirmationService;
10 use BookStack\Services\SocialAuthService; 9 use BookStack\Services\SocialAuthService;
11 -use BookStack\SocialAccount; 10 +use BookStack\User;
11 +use Exception;
12 +use Illuminate\Http\Request;
13 +use Illuminate\Http\Response;
12 use Validator; 14 use Validator;
13 use BookStack\Http\Controllers\Controller; 15 use BookStack\Http\Controllers\Controller;
14 -use Illuminate\Foundation\Auth\ThrottlesLogins; 16 +use Illuminate\Foundation\Auth\RegistersUsers;
15 -use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
16 17
17 -class AuthController extends Controller 18 +class RegisterController extends Controller
18 { 19 {
19 /* 20 /*
20 |-------------------------------------------------------------------------- 21 |--------------------------------------------------------------------------
21 - | Registration & Login Controller 22 + | Register Controller
22 |-------------------------------------------------------------------------- 23 |--------------------------------------------------------------------------
23 | 24 |
24 - | This controller handles the registration of new users, as well as the 25 + | This controller handles the registration of new users as well as their
25 - | authentication of existing users. By default, this controller uses 26 + | validation and creation. By default this controller uses a trait to
26 - | a simple trait to add these behaviors. Why don't you explore it? 27 + | provide this functionality without requiring any additional code.
27 | 28 |
28 */ 29 */
29 30
30 - use AuthenticatesAndRegistersUsers, ThrottlesLogins; 31 + use RegistersUsers;
31 -
32 - protected $redirectPath = '/';
33 - protected $redirectAfterLogout = '/login';
34 - protected $username = 'email';
35 32
36 protected $socialAuthService; 33 protected $socialAuthService;
37 protected $emailConfirmationService; 34 protected $emailConfirmationService;
38 protected $userRepo; 35 protected $userRepo;
39 36
40 /** 37 /**
41 - * Create a new authentication controller instance. 38 + * Where to redirect users after login / registration.
39 + *
40 + * @var string
41 + */
42 + protected $redirectTo = '/';
43 + protected $redirectPath = '/';
44 +
45 + /**
46 + * Create a new controller instance.
47 + *
42 * @param SocialAuthService $socialAuthService 48 * @param SocialAuthService $socialAuthService
43 * @param EmailConfirmationService $emailConfirmationService 49 * @param EmailConfirmationService $emailConfirmationService
44 * @param UserRepo $userRepo 50 * @param UserRepo $userRepo
45 */ 51 */
46 public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) 52 public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
47 { 53 {
48 - $this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]); 54 + $this->middleware('guest');
49 $this->socialAuthService = $socialAuthService; 55 $this->socialAuthService = $socialAuthService;
50 $this->emailConfirmationService = $emailConfirmationService; 56 $this->emailConfirmationService = $emailConfirmationService;
51 $this->userRepo = $userRepo; 57 $this->userRepo = $userRepo;
58 + $this->redirectTo = baseUrl('/');
52 $this->redirectPath = baseUrl('/'); 59 $this->redirectPath = baseUrl('/');
53 - $this->redirectAfterLogout = baseUrl('/login');
54 - $this->username = config('auth.method') === 'standard' ? 'email' : 'username';
55 parent::__construct(); 60 parent::__construct();
56 } 61 }
57 62
58 /** 63 /**
59 * Get a validator for an incoming registration request. 64 * Get a validator for an incoming registration request.
65 + *
60 * @param array $data 66 * @param array $data
61 * @return \Illuminate\Contracts\Validation\Validator 67 * @return \Illuminate\Contracts\Validation\Validator
62 */ 68 */
...@@ -69,6 +75,10 @@ class AuthController extends Controller ...@@ -69,6 +75,10 @@ class AuthController extends Controller
69 ]); 75 ]);
70 } 76 }
71 77
78 + /**
79 + * Check whether or not registrations are allowed in the app settings.
80 + * @throws UserRegistrationException
81 + */
72 protected function checkRegistrationAllowed() 82 protected function checkRegistrationAllowed()
73 { 83 {
74 if (!setting('registration-enabled')) { 84 if (!setting('registration-enabled')) {
...@@ -78,7 +88,7 @@ class AuthController extends Controller ...@@ -78,7 +88,7 @@ class AuthController extends Controller
78 88
79 /** 89 /**
80 * Show the application registration form. 90 * Show the application registration form.
81 - * @return \Illuminate\Http\Response 91 + * @return Response
82 */ 92 */
83 public function getRegister() 93 public function getRegister()
84 { 94 {
...@@ -89,9 +99,10 @@ class AuthController extends Controller ...@@ -89,9 +99,10 @@ class AuthController extends Controller
89 99
90 /** 100 /**
91 * Handle a registration request for the application. 101 * Handle a registration request for the application.
92 - * @param \Illuminate\Http\Request $request 102 + * @param Request|\Illuminate\Http\Request $request
93 - * @return \Illuminate\Http\Response 103 + * @return Response
94 * @throws UserRegistrationException 104 * @throws UserRegistrationException
105 + * @throws \Illuminate\Foundation\Validation\ValidationException
95 */ 106 */
96 public function postRegister(Request $request) 107 public function postRegister(Request $request)
97 { 108 {
...@@ -108,66 +119,18 @@ class AuthController extends Controller ...@@ -108,66 +119,18 @@ class AuthController extends Controller
108 return $this->registerUser($userData); 119 return $this->registerUser($userData);
109 } 120 }
110 121
111 -
112 - /**
113 - * Overrides the action when a user is authenticated.
114 - * If the user authenticated but does not exist in the user table we create them.
115 - * @param Request $request
116 - * @param Authenticatable $user
117 - * @return \Illuminate\Http\RedirectResponse
118 - * @throws AuthException
119 - */
120 - protected function authenticated(Request $request, Authenticatable $user)
121 - {
122 - // Explicitly log them out for now if they do no exist.
123 - if (!$user->exists) auth()->logout($user);
124 -
125 - if (!$user->exists && $user->email === null && !$request->has('email')) {
126 - $request->flash();
127 - session()->flash('request-email', true);
128 - return redirect('/login');
129 - }
130 -
131 - if (!$user->exists && $user->email === null && $request->has('email')) {
132 - $user->email = $request->get('email');
133 - }
134 -
135 - if (!$user->exists) {
136 -
137 - // Check for users with same email already
138 - $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
139 - if ($alreadyUser) {
140 - throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
141 - }
142 -
143 - $user->save();
144 - $this->userRepo->attachDefaultRole($user);
145 - auth()->login($user);
146 - }
147 -
148 - $path = session()->pull('url.intended', '/');
149 - $path = baseUrl($path, true);
150 - return redirect($path);
151 - }
152 -
153 /** 122 /**
154 - * Register a new user after a registration callback. 123 + * Create a new user instance after a valid registration.
155 - * @param $socialDriver 124 + * @param array $data
156 - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 125 + * @return User
157 - * @throws UserRegistrationException
158 */ 126 */
159 - protected function socialRegisterCallback($socialDriver) 127 + protected function create(array $data)
160 { 128 {
161 - $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver); 129 + return User::create([
162 - $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser); 130 + 'name' => $data['name'],
163 - 131 + 'email' => $data['email'],
164 - // Create an array of the user data to create a new user instance 132 + 'password' => bcrypt($data['password']),
165 - $userData = [ 133 + ]);
166 - 'name' => $socialUser->getName(),
167 - 'email' => $socialUser->getEmail(),
168 - 'password' => str_random(30)
169 - ];
170 - return $this->registerUser($userData, $socialAccount);
171 } 134 }
172 135
173 /** 136 /**
...@@ -176,7 +139,7 @@ class AuthController extends Controller ...@@ -176,7 +139,7 @@ class AuthController extends Controller
176 * @param bool|false|SocialAccount $socialAccount 139 * @param bool|false|SocialAccount $socialAccount
177 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 140 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
178 * @throws UserRegistrationException 141 * @throws UserRegistrationException
179 - * @throws \BookStack\Exceptions\ConfirmationEmailException 142 + * @throws ConfirmationEmailException
180 */ 143 */
181 protected function registerUser(array $userData, $socialAccount = false) 144 protected function registerUser(array $userData, $socialAccount = false)
182 { 145 {
...@@ -195,7 +158,13 @@ class AuthController extends Controller ...@@ -195,7 +158,13 @@ class AuthController extends Controller
195 158
196 if (setting('registration-confirmation') || setting('registration-restrict')) { 159 if (setting('registration-confirmation') || setting('registration-restrict')) {
197 $newUser->save(); 160 $newUser->save();
161 +
162 + try {
198 $this->emailConfirmationService->sendConfirmation($newUser); 163 $this->emailConfirmationService->sendConfirmation($newUser);
164 + } catch (Exception $e) {
165 + session()->flash('error', trans('auth.email_confirm_send_error'));
166 + }
167 +
199 return redirect('/register/confirm'); 168 return redirect('/register/confirm');
200 } 169 }
201 170
...@@ -214,18 +183,6 @@ class AuthController extends Controller ...@@ -214,18 +183,6 @@ class AuthController extends Controller
214 } 183 }
215 184
216 /** 185 /**
217 - * View the confirmation email as a standard web page.
218 - * @param $token
219 - * @return \Illuminate\View\View
220 - * @throws UserRegistrationException
221 - */
222 - public function viewConfirmEmail($token)
223 - {
224 - $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
225 - return view('emails/email-confirmation', ['token' => $confirmation->token]);
226 - }
227 -
228 - /**
229 * Confirms an email via a token and logs the user into the system. 186 * Confirms an email via a token and logs the user into the system.
230 * @param $token 187 * @param $token
231 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 188 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
...@@ -237,8 +194,8 @@ class AuthController extends Controller ...@@ -237,8 +194,8 @@ class AuthController extends Controller
237 $user = $confirmation->user; 194 $user = $confirmation->user;
238 $user->email_confirmed = true; 195 $user->email_confirmed = true;
239 $user->save(); 196 $user->save();
240 - auth()->login($confirmation->user); 197 + auth()->login($user);
241 - session()->flash('success', 'Your email has been confirmed!'); 198 + session()->flash('success', trans('auth.email_confirm_success'));
242 $this->emailConfirmationService->deleteConfirmationsByUser($user); 199 $this->emailConfirmationService->deleteConfirmationsByUser($user);
243 return redirect($this->redirectPath); 200 return redirect($this->redirectPath);
244 } 201 }
...@@ -264,31 +221,17 @@ class AuthController extends Controller ...@@ -264,31 +221,17 @@ class AuthController extends Controller
264 'email' => 'required|email|exists:users,email' 221 'email' => 'required|email|exists:users,email'
265 ]); 222 ]);
266 $user = $this->userRepo->getByEmail($request->get('email')); 223 $user = $this->userRepo->getByEmail($request->get('email'));
224 +
225 + try {
267 $this->emailConfirmationService->sendConfirmation($user); 226 $this->emailConfirmationService->sendConfirmation($user);
268 - session()->flash('success', 'Confirmation email resent, Please check your inbox.'); 227 + } catch (Exception $e) {
228 + session()->flash('error', trans('auth.email_confirm_send_error'));
269 return redirect('/register/confirm'); 229 return redirect('/register/confirm');
270 } 230 }
271 231
272 - /** 232 + $this->emailConfirmationService->sendConfirmation($user);
273 - * Show the application login form. 233 + session()->flash('success', trans('auth.email_confirm_resent'));
274 - * @return \Illuminate\Http\Response 234 + return redirect('/register/confirm');
275 - */
276 - public function getLogin()
277 - {
278 - $socialDrivers = $this->socialAuthService->getActiveDrivers();
279 - $authMethod = config('auth.method');
280 - return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
281 - }
282 -
283 - /**
284 - * Redirect to the relevant social site.
285 - * @param $socialDriver
286 - * @return \Symfony\Component\HttpFoundation\RedirectResponse
287 - */
288 - public function getSocialLogin($socialDriver)
289 - {
290 - session()->put('social-callback', 'login');
291 - return $this->socialAuthService->startLogIn($socialDriver);
292 } 235 }
293 236
294 /** 237 /**
...@@ -334,4 +277,25 @@ class AuthController extends Controller ...@@ -334,4 +277,25 @@ class AuthController extends Controller
334 return $this->socialAuthService->detachSocialAccount($socialDriver); 277 return $this->socialAuthService->detachSocialAccount($socialDriver);
335 } 278 }
336 279
280 + /**
281 + * Register a new user after a registration callback.
282 + * @param $socialDriver
283 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
284 + * @throws UserRegistrationException
285 + */
286 + protected function socialRegisterCallback($socialDriver)
287 + {
288 + $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
289 + $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
290 +
291 + // Create an array of the user data to create a new user instance
292 + $userData = [
293 + 'name' => $socialUser->getName(),
294 + 'email' => $socialUser->getEmail(),
295 + 'password' => str_random(30)
296 + ];
297 + return $this->registerUser($userData, $socialAccount);
298 + }
299 +
300 +
337 } 301 }
...\ 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 }
......
...@@ -12,6 +12,7 @@ use BookStack\Repos\ChapterRepo; ...@@ -12,6 +12,7 @@ use BookStack\Repos\ChapterRepo;
12 use BookStack\Repos\PageRepo; 12 use BookStack\Repos\PageRepo;
13 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 13 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
14 use Views; 14 use Views;
15 +use GatherContent\Htmldiff\Htmldiff;
15 16
16 class PageController extends Controller 17 class PageController extends Controller
17 { 18 {
...@@ -42,27 +43,60 @@ class PageController extends Controller ...@@ -42,27 +43,60 @@ class PageController extends Controller
42 43
43 /** 44 /**
44 * Show the form for creating a new page. 45 * Show the form for creating a new page.
45 - * @param $bookSlug 46 + * @param string $bookSlug
46 - * @param bool $chapterSlug 47 + * @param string $chapterSlug
47 * @return Response 48 * @return Response
48 * @internal param bool $pageSlug 49 * @internal param bool $pageSlug
49 */ 50 */
50 - public function create($bookSlug, $chapterSlug = false) 51 + public function create($bookSlug, $chapterSlug = null)
51 { 52 {
52 $book = $this->bookRepo->getBySlug($bookSlug); 53 $book = $this->bookRepo->getBySlug($bookSlug);
53 $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; 54 $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
54 $parent = $chapter ? $chapter : $book; 55 $parent = $chapter ? $chapter : $book;
55 $this->checkOwnablePermission('page-create', $parent); 56 $this->checkOwnablePermission('page-create', $parent);
56 - $this->setPageTitle('Create New Page');
57 57
58 + // Redirect to draft edit screen if signed in
59 + if ($this->signedIn) {
58 $draft = $this->pageRepo->getDraftPage($book, $chapter); 60 $draft = $this->pageRepo->getDraftPage($book, $chapter);
59 return redirect($draft->getUrl()); 61 return redirect($draft->getUrl());
60 } 62 }
61 63
64 + // Otherwise show edit view
65 + $this->setPageTitle('Create New Page');
66 + return view('pages/guest-create', ['parent' => $parent]);
67 + }
68 +
69 + /**
70 + * Create a new page as a guest user.
71 + * @param Request $request
72 + * @param string $bookSlug
73 + * @param string|null $chapterSlug
74 + * @return mixed
75 + * @throws NotFoundException
76 + */
77 + public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
78 + {
79 + $this->validate($request, [
80 + 'name' => 'required|string|max:255'
81 + ]);
82 +
83 + $book = $this->bookRepo->getBySlug($bookSlug);
84 + $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
85 + $parent = $chapter ? $chapter : $book;
86 + $this->checkOwnablePermission('page-create', $parent);
87 +
88 + $page = $this->pageRepo->getDraftPage($book, $chapter);
89 + $this->pageRepo->publishDraft($page, [
90 + 'name' => $request->get('name'),
91 + 'html' => ''
92 + ]);
93 + return redirect($page->getUrl('/edit'));
94 + }
95 +
62 /** 96 /**
63 * Show form to continue editing a draft page. 97 * Show form to continue editing a draft page.
64 - * @param $bookSlug 98 + * @param string $bookSlug
65 - * @param $pageId 99 + * @param int $pageId
66 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 100 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
67 */ 101 */
68 public function editDraft($bookSlug, $pageId) 102 public function editDraft($bookSlug, $pageId)
...@@ -72,7 +106,13 @@ class PageController extends Controller ...@@ -72,7 +106,13 @@ class PageController extends Controller
72 $this->checkOwnablePermission('page-create', $book); 106 $this->checkOwnablePermission('page-create', $book);
73 $this->setPageTitle('Edit Page Draft'); 107 $this->setPageTitle('Edit Page Draft');
74 108
75 - return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]); 109 + $draftsEnabled = $this->signedIn;
110 + return view('pages/edit', [
111 + 'page' => $draft,
112 + 'book' => $book,
113 + 'isDraft' => true,
114 + 'draftsEnabled' => $draftsEnabled
115 + ]);
76 } 116 }
77 117
78 /** 118 /**
...@@ -112,8 +152,8 @@ class PageController extends Controller ...@@ -112,8 +152,8 @@ class PageController extends Controller
112 * Display the specified page. 152 * Display the specified page.
113 * If the page is not found via the slug the 153 * If the page is not found via the slug the
114 * revisions are searched for a match. 154 * revisions are searched for a match.
115 - * @param $bookSlug 155 + * @param string $bookSlug
116 - * @param $pageSlug 156 + * @param string $pageSlug
117 * @return Response 157 * @return Response
118 */ 158 */
119 public function show($bookSlug, $pageSlug) 159 public function show($bookSlug, $pageSlug)
...@@ -131,14 +171,17 @@ class PageController extends Controller ...@@ -131,14 +171,17 @@ class PageController extends Controller
131 $this->checkOwnablePermission('page-view', $page); 171 $this->checkOwnablePermission('page-view', $page);
132 172
133 $sidebarTree = $this->bookRepo->getChildren($book); 173 $sidebarTree = $this->bookRepo->getChildren($book);
174 + $pageNav = $this->pageRepo->getPageNav($page);
175 +
134 Views::add($page); 176 Views::add($page);
135 $this->setPageTitle($page->getShortName()); 177 $this->setPageTitle($page->getShortName());
136 - return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]); 178 + return view('pages/show', ['page' => $page, 'book' => $book,
179 + 'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]);
137 } 180 }
138 181
139 /** 182 /**
140 * Get page from an ajax request. 183 * Get page from an ajax request.
141 - * @param $pageId 184 + * @param int $pageId
142 * @return \Illuminate\Http\JsonResponse 185 * @return \Illuminate\Http\JsonResponse
143 */ 186 */
144 public function getPageAjax($pageId) 187 public function getPageAjax($pageId)
...@@ -149,8 +192,8 @@ class PageController extends Controller ...@@ -149,8 +192,8 @@ class PageController extends Controller
149 192
150 /** 193 /**
151 * Show the form for editing the specified page. 194 * Show the form for editing the specified page.
152 - * @param $bookSlug 195 + * @param string $bookSlug
153 - * @param $pageSlug 196 + * @param string $pageSlug
154 * @return Response 197 * @return Response
155 */ 198 */
156 public function edit($bookSlug, $pageSlug) 199 public function edit($bookSlug, $pageSlug)
...@@ -179,14 +222,20 @@ class PageController extends Controller ...@@ -179,14 +222,20 @@ class PageController extends Controller
179 222
180 if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings)); 223 if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
181 224
182 - return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); 225 + $draftsEnabled = $this->signedIn;
226 + return view('pages/edit', [
227 + 'page' => $page,
228 + 'book' => $book,
229 + 'current' => $page,
230 + 'draftsEnabled' => $draftsEnabled
231 + ]);
183 } 232 }
184 233
185 /** 234 /**
186 * Update the specified page in storage. 235 * Update the specified page in storage.
187 * @param Request $request 236 * @param Request $request
188 - * @param $bookSlug 237 + * @param string $bookSlug
189 - * @param $pageSlug 238 + * @param string $pageSlug
190 * @return Response 239 * @return Response
191 */ 240 */
192 public function update(Request $request, $bookSlug, $pageSlug) 241 public function update(Request $request, $bookSlug, $pageSlug)
...@@ -205,13 +254,21 @@ class PageController extends Controller ...@@ -205,13 +254,21 @@ class PageController extends Controller
205 /** 254 /**
206 * Save a draft update as a revision. 255 * Save a draft update as a revision.
207 * @param Request $request 256 * @param Request $request
208 - * @param $pageId 257 + * @param int $pageId
209 * @return \Illuminate\Http\JsonResponse 258 * @return \Illuminate\Http\JsonResponse
210 */ 259 */
211 public function saveDraft(Request $request, $pageId) 260 public function saveDraft(Request $request, $pageId)
212 { 261 {
213 $page = $this->pageRepo->getById($pageId, true); 262 $page = $this->pageRepo->getById($pageId, true);
214 $this->checkOwnablePermission('page-update', $page); 263 $this->checkOwnablePermission('page-update', $page);
264 +
265 + if (!$this->signedIn) {
266 + return response()->json([
267 + 'status' => 'error',
268 + 'message' => 'Guests cannot save drafts',
269 + ], 500);
270 + }
271 +
215 if ($page->draft) { 272 if ($page->draft) {
216 $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown'])); 273 $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
217 } else { 274 } else {
...@@ -230,7 +287,7 @@ class PageController extends Controller ...@@ -230,7 +287,7 @@ class PageController extends Controller
230 /** 287 /**
231 * Redirect from a special link url which 288 * Redirect from a special link url which
232 * uses the page id rather than the name. 289 * uses the page id rather than the name.
233 - * @param $pageId 290 + * @param int $pageId
234 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 291 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
235 */ 292 */
236 public function redirectFromLink($pageId) 293 public function redirectFromLink($pageId)
...@@ -241,8 +298,8 @@ class PageController extends Controller ...@@ -241,8 +298,8 @@ class PageController extends Controller
241 298
242 /** 299 /**
243 * Show the deletion page for the specified page. 300 * Show the deletion page for the specified page.
244 - * @param $bookSlug 301 + * @param string $bookSlug
245 - * @param $pageSlug 302 + * @param string $pageSlug
246 * @return \Illuminate\View\View 303 * @return \Illuminate\View\View
247 */ 304 */
248 public function showDelete($bookSlug, $pageSlug) 305 public function showDelete($bookSlug, $pageSlug)
...@@ -257,8 +314,8 @@ class PageController extends Controller ...@@ -257,8 +314,8 @@ class PageController extends Controller
257 314
258 /** 315 /**
259 * Show the deletion page for the specified page. 316 * Show the deletion page for the specified page.
260 - * @param $bookSlug 317 + * @param string $bookSlug
261 - * @param $pageId 318 + * @param int $pageId
262 * @return \Illuminate\View\View 319 * @return \Illuminate\View\View
263 * @throws NotFoundException 320 * @throws NotFoundException
264 */ 321 */
...@@ -273,8 +330,8 @@ class PageController extends Controller ...@@ -273,8 +330,8 @@ class PageController extends Controller
273 330
274 /** 331 /**
275 * Remove the specified page from storage. 332 * Remove the specified page from storage.
276 - * @param $bookSlug 333 + * @param string $bookSlug
277 - * @param $pageSlug 334 + * @param string $pageSlug
278 * @return Response 335 * @return Response
279 * @internal param int $id 336 * @internal param int $id
280 */ 337 */
...@@ -291,8 +348,8 @@ class PageController extends Controller ...@@ -291,8 +348,8 @@ class PageController extends Controller
291 348
292 /** 349 /**
293 * Remove the specified draft page from storage. 350 * Remove the specified draft page from storage.
294 - * @param $bookSlug 351 + * @param string $bookSlug
295 - * @param $pageId 352 + * @param int $pageId
296 * @return Response 353 * @return Response
297 * @throws NotFoundException 354 * @throws NotFoundException
298 */ 355 */
...@@ -308,8 +365,8 @@ class PageController extends Controller ...@@ -308,8 +365,8 @@ class PageController extends Controller
308 365
309 /** 366 /**
310 * Shows the last revisions for this page. 367 * Shows the last revisions for this page.
311 - * @param $bookSlug 368 + * @param string $bookSlug
312 - * @param $pageSlug 369 + * @param string $pageSlug
313 * @return \Illuminate\View\View 370 * @return \Illuminate\View\View
314 */ 371 */
315 public function showRevisions($bookSlug, $pageSlug) 372 public function showRevisions($bookSlug, $pageSlug)
...@@ -322,9 +379,9 @@ class PageController extends Controller ...@@ -322,9 +379,9 @@ class PageController extends Controller
322 379
323 /** 380 /**
324 * Shows a preview of a single revision 381 * Shows a preview of a single revision
325 - * @param $bookSlug 382 + * @param string $bookSlug
326 - * @param $pageSlug 383 + * @param string $pageSlug
327 - * @param $revisionId 384 + * @param int $revisionId
328 * @return \Illuminate\View\View 385 * @return \Illuminate\View\View
329 */ 386 */
330 public function showRevision($bookSlug, $pageSlug, $revisionId) 387 public function showRevision($bookSlug, $pageSlug, $revisionId)
...@@ -332,16 +389,48 @@ class PageController extends Controller ...@@ -332,16 +389,48 @@ class PageController extends Controller
332 $book = $this->bookRepo->getBySlug($bookSlug); 389 $book = $this->bookRepo->getBySlug($bookSlug);
333 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 390 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
334 $revision = $this->pageRepo->getRevisionById($revisionId); 391 $revision = $this->pageRepo->getRevisionById($revisionId);
392 +
335 $page->fill($revision->toArray()); 393 $page->fill($revision->toArray());
336 $this->setPageTitle('Page Revision For ' . $page->getShortName()); 394 $this->setPageTitle('Page Revision For ' . $page->getShortName());
337 - return view('pages/revision', ['page' => $page, 'book' => $book]); 395 +
396 + return view('pages/revision', [
397 + 'page' => $page,
398 + 'book' => $book,
399 + ]);
400 + }
401 +
402 + /**
403 + * Shows the changes of a single revision
404 + * @param string $bookSlug
405 + * @param string $pageSlug
406 + * @param int $revisionId
407 + * @return \Illuminate\View\View
408 + */
409 + public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
410 + {
411 + $book = $this->bookRepo->getBySlug($bookSlug);
412 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
413 + $revision = $this->pageRepo->getRevisionById($revisionId);
414 +
415 + $prev = $revision->getPrevious();
416 + $prevContent = ($prev === null) ? '' : $prev->html;
417 + $diff = (new Htmldiff)->diff($prevContent, $revision->html);
418 +
419 + $page->fill($revision->toArray());
420 + $this->setPageTitle('Page Revision For ' . $page->getShortName());
421 +
422 + return view('pages/revision', [
423 + 'page' => $page,
424 + 'book' => $book,
425 + 'diff' => $diff,
426 + ]);
338 } 427 }
339 428
340 /** 429 /**
341 * Restores a page using the content of the specified revision. 430 * Restores a page using the content of the specified revision.
342 - * @param $bookSlug 431 + * @param string $bookSlug
343 - * @param $pageSlug 432 + * @param string $pageSlug
344 - * @param $revisionId 433 + * @param int $revisionId
345 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 434 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
346 */ 435 */
347 public function restoreRevision($bookSlug, $pageSlug, $revisionId) 436 public function restoreRevision($bookSlug, $pageSlug, $revisionId)
...@@ -357,8 +446,8 @@ class PageController extends Controller ...@@ -357,8 +446,8 @@ class PageController extends Controller
357 /** 446 /**
358 * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper. 447 * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
359 * https://github.com/barryvdh/laravel-dompdf 448 * https://github.com/barryvdh/laravel-dompdf
360 - * @param $bookSlug 449 + * @param string $bookSlug
361 - * @param $pageSlug 450 + * @param string $pageSlug
362 * @return \Illuminate\Http\Response 451 * @return \Illuminate\Http\Response
363 */ 452 */
364 public function exportPdf($bookSlug, $pageSlug) 453 public function exportPdf($bookSlug, $pageSlug)
...@@ -374,8 +463,8 @@ class PageController extends Controller ...@@ -374,8 +463,8 @@ class PageController extends Controller
374 463
375 /** 464 /**
376 * Export a page to a self-contained HTML file. 465 * Export a page to a self-contained HTML file.
377 - * @param $bookSlug 466 + * @param string $bookSlug
378 - * @param $pageSlug 467 + * @param string $pageSlug
379 * @return \Illuminate\Http\Response 468 * @return \Illuminate\Http\Response
380 */ 469 */
381 public function exportHtml($bookSlug, $pageSlug) 470 public function exportHtml($bookSlug, $pageSlug)
...@@ -391,8 +480,8 @@ class PageController extends Controller ...@@ -391,8 +480,8 @@ class PageController extends Controller
391 480
392 /** 481 /**
393 * Export a page to a simple plaintext .txt file. 482 * Export a page to a simple plaintext .txt file.
394 - * @param $bookSlug 483 + * @param string $bookSlug
395 - * @param $pageSlug 484 + * @param string $pageSlug
396 * @return \Illuminate\Http\Response 485 * @return \Illuminate\Http\Response
397 */ 486 */
398 public function exportPlainText($bookSlug, $pageSlug) 487 public function exportPlainText($bookSlug, $pageSlug)
...@@ -434,8 +523,8 @@ class PageController extends Controller ...@@ -434,8 +523,8 @@ class PageController extends Controller
434 523
435 /** 524 /**
436 * Show the Restrictions view. 525 * Show the Restrictions view.
437 - * @param $bookSlug 526 + * @param string $bookSlug
438 - * @param $pageSlug 527 + * @param string $pageSlug
439 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 528 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
440 */ 529 */
441 public function showRestrict($bookSlug, $pageSlug) 530 public function showRestrict($bookSlug, $pageSlug)
...@@ -452,8 +541,8 @@ class PageController extends Controller ...@@ -452,8 +541,8 @@ class PageController extends Controller
452 541
453 /** 542 /**
454 * Show the view to choose a new parent to move a page into. 543 * Show the view to choose a new parent to move a page into.
455 - * @param $bookSlug 544 + * @param string $bookSlug
456 - * @param $pageSlug 545 + * @param string $pageSlug
457 * @return mixed 546 * @return mixed
458 * @throws NotFoundException 547 * @throws NotFoundException
459 */ 548 */
...@@ -470,8 +559,8 @@ class PageController extends Controller ...@@ -470,8 +559,8 @@ class PageController extends Controller
470 559
471 /** 560 /**
472 * Does the action of moving the location of a page 561 * Does the action of moving the location of a page
473 - * @param $bookSlug 562 + * @param string $bookSlug
474 - * @param $pageSlug 563 + * @param string $pageSlug
475 * @param Request $request 564 * @param Request $request
476 * @return mixed 565 * @return mixed
477 * @throws NotFoundException 566 * @throws NotFoundException
...@@ -513,8 +602,8 @@ class PageController extends Controller ...@@ -513,8 +602,8 @@ class PageController extends Controller
513 602
514 /** 603 /**
515 * Set the permissions for this page. 604 * Set the permissions for this page.
516 - * @param $bookSlug 605 + * @param string $bookSlug
517 - * @param $pageSlug 606 + * @param string $pageSlug
518 * @param Request $request 607 * @param Request $request
519 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 608 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
520 */ 609 */
......
...@@ -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')) {
104 + try {
103 $avatar = \Images::saveUserGravatar($user); 105 $avatar = \Images::saveUserGravatar($user);
104 $user->avatar()->associate($avatar); 106 $user->avatar()->associate($avatar);
105 $user->save(); 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,
18 + ];
19 +
20 + /**
21 + * The application's route middleware groups.
22 + *
23 + * @var array
24 + */
25 + protected $middlewareGroups = [
26 + 'web' => [
16 \BookStack\Http\Middleware\EncryptCookies::class, 27 \BookStack\Http\Middleware\EncryptCookies::class,
17 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 28 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
18 \Illuminate\Session\Middleware\StartSession::class, 29 \Illuminate\Session\Middleware\StartSession::class,
19 \Illuminate\View\Middleware\ShareErrorsFromSession::class, 30 \Illuminate\View\Middleware\ShareErrorsFromSession::class,
20 \BookStack\Http\Middleware\VerifyCsrfToken::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 {
195 + try {
194 return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); 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')) {
88 + try {
87 $avatar = \Images::saveUserGravatar($user); 89 $avatar = \Images::saveUserGravatar($user);
88 $user->avatar()->associate($avatar); 90 $user->avatar()->associate($avatar);
89 $user->save(); 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));
......
...@@ -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
1 "use strict"; 1 "use strict";
2 2
3 -const moment = require('moment'); 3 +import moment from 'moment';
4 +import 'moment/locale/en-gb';
5 +moment.locale('en-gb');
4 6
5 -module.exports = function (ngApp, events) { 7 +export default function (ngApp, events) {
6 8
7 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', 9 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
8 function ($scope, $attrs, $http, $timeout, imageManagerService) { 10 function ($scope, $attrs, $http, $timeout, imageManagerService) {
...@@ -162,7 +164,6 @@ module.exports = function (ngApp, events) { ...@@ -162,7 +164,6 @@ module.exports = function (ngApp, events) {
162 164
163 /** 165 /**
164 * Start a search operation 166 * Start a search operation
165 - * @param searchTerm
166 */ 167 */
167 $scope.searchImages = function() { 168 $scope.searchImages = function() {
168 169
...@@ -196,7 +197,7 @@ module.exports = function (ngApp, events) { ...@@ -196,7 +197,7 @@ module.exports = function (ngApp, events) {
196 $scope.view = viewName; 197 $scope.view = viewName;
197 baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/'); 198 baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
198 fetchData(); 199 fetchData();
199 - } 200 + };
200 201
201 /** 202 /**
202 * Save the details of an image. 203 * Save the details of an image.
...@@ -205,7 +206,7 @@ module.exports = function (ngApp, events) { ...@@ -205,7 +206,7 @@ module.exports = function (ngApp, events) {
205 $scope.saveImageDetails = function (event) { 206 $scope.saveImageDetails = function (event) {
206 event.preventDefault(); 207 event.preventDefault();
207 var url = window.baseUrl('/images/update/' + $scope.selectedImage.id); 208 var url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
208 - $http.put(url, this.selectedImage).then((response) => { 209 + $http.put(url, this.selectedImage).then(response => {
209 events.emit('success', 'Image details updated'); 210 events.emit('success', 'Image details updated');
210 }, (response) => { 211 }, (response) => {
211 if (response.status === 422) { 212 if (response.status === 422) {
...@@ -300,15 +301,16 @@ module.exports = function (ngApp, events) { ...@@ -300,15 +301,16 @@ module.exports = function (ngApp, events) {
300 var isEdit = pageId !== 0; 301 var isEdit = pageId !== 0;
301 var autosaveFrequency = 30; // AutoSave interval in seconds. 302 var autosaveFrequency = 30; // AutoSave interval in seconds.
302 var isMarkdown = $attrs.editorType === 'markdown'; 303 var isMarkdown = $attrs.editorType === 'markdown';
304 + $scope.draftsEnabled = $attrs.draftsEnabled === 'true';
303 $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1; 305 $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
304 $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1; 306 $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
305 307
306 - // Set inital header draft text 308 + // Set initial header draft text
307 if ($scope.isUpdateDraft || $scope.isNewPageDraft) { 309 if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
308 $scope.draftText = 'Editing Draft' 310 $scope.draftText = 'Editing Draft'
309 } else { 311 } else {
310 $scope.draftText = 'Editing Page' 312 $scope.draftText = 'Editing Page'
311 - }; 313 + }
312 314
313 var autoSave = false; 315 var autoSave = false;
314 316
...@@ -317,7 +319,7 @@ module.exports = function (ngApp, events) { ...@@ -317,7 +319,7 @@ module.exports = function (ngApp, events) {
317 html: false 319 html: false
318 }; 320 };
319 321
320 - if (isEdit) { 322 + if (isEdit && $scope.draftsEnabled) {
321 setTimeout(() => { 323 setTimeout(() => {
322 startAutoSave(); 324 startAutoSave();
323 }, 1000); 325 }, 1000);
...@@ -336,6 +338,8 @@ module.exports = function (ngApp, events) { ...@@ -336,6 +338,8 @@ module.exports = function (ngApp, events) {
336 $scope.editorChange = function() {}; 338 $scope.editorChange = function() {};
337 } 339 }
338 340
341 + let lastSave = 0;
342 +
339 /** 343 /**
340 * Start the AutoSave loop, Checks for content change 344 * Start the AutoSave loop, Checks for content change
341 * before performing the costly AJAX request. 345 * before performing the costly AJAX request.
...@@ -345,6 +349,8 @@ module.exports = function (ngApp, events) { ...@@ -345,6 +349,8 @@ module.exports = function (ngApp, events) {
345 currentContent.html = $scope.editContent; 349 currentContent.html = $scope.editContent;
346 350
347 autoSave = $interval(() => { 351 autoSave = $interval(() => {
352 + // Return if manually saved recently to prevent bombarding the server
353 + if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return;
348 var newTitle = $('#name').val(); 354 var newTitle = $('#name').val();
349 var newHtml = $scope.editContent; 355 var newHtml = $scope.editContent;
350 356
...@@ -357,10 +363,12 @@ module.exports = function (ngApp, events) { ...@@ -357,10 +363,12 @@ module.exports = function (ngApp, events) {
357 }, 1000 * autosaveFrequency); 363 }, 1000 * autosaveFrequency);
358 } 364 }
359 365
366 + let draftErroring = false;
360 /** 367 /**
361 * Save a draft update into the system via an AJAX request. 368 * Save a draft update into the system via an AJAX request.
362 */ 369 */
363 function saveDraft() { 370 function saveDraft() {
371 + if (!$scope.draftsEnabled) return;
364 var data = { 372 var data = {
365 name: $('#name').val(), 373 name: $('#name').val(),
366 html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent 374 html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
...@@ -369,11 +377,17 @@ module.exports = function (ngApp, events) { ...@@ -369,11 +377,17 @@ module.exports = function (ngApp, events) {
369 if (isMarkdown) data.markdown = $scope.editContent; 377 if (isMarkdown) data.markdown = $scope.editContent;
370 378
371 let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft'); 379 let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
372 - $http.put(url, data).then((responseData) => { 380 + $http.put(url, data).then(responseData => {
381 + draftErroring = false;
373 var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate(); 382 var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
374 $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm'); 383 $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
375 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; 384 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
376 showDraftSaveNotification(); 385 showDraftSaveNotification();
386 + lastSave = Date.now();
387 + }, errorRes => {
388 + if (draftErroring) return;
389 + events.emit('error', 'Failed to save draft. Ensure you have internet connection before saving this page.')
390 + draftErroring = true;
377 }); 391 });
378 } 392 }
379 393
...@@ -447,7 +461,7 @@ module.exports = function (ngApp, events) { ...@@ -447,7 +461,7 @@ module.exports = function (ngApp, events) {
447 * Get all tags for the current book and add into scope. 461 * Get all tags for the current book and add into scope.
448 */ 462 */
449 function getTags() { 463 function getTags() {
450 - let url = window.baseUrl('/ajax/tags/get/page/' + pageId); 464 + let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
451 $http.get(url).then((responseData) => { 465 $http.get(url).then((responseData) => {
452 $scope.tags = responseData.data; 466 $scope.tags = responseData.data;
453 addEmptyTag(); 467 addEmptyTag();
...@@ -516,21 +530,201 @@ module.exports = function (ngApp, events) { ...@@ -516,21 +530,201 @@ module.exports = function (ngApp, events) {
516 530
517 }]); 531 }]);
518 532
519 -};
520 533
534 + ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
535 + function ($scope, $http, $attrs) {
521 536
537 + const pageId = $scope.uploadedTo = $attrs.pageId;
538 + let currentOrder = '';
539 + $scope.files = [];
540 + $scope.editFile = false;
541 + $scope.file = getCleanFile();
542 + $scope.errors = {
543 + link: {},
544 + edit: {}
545 + };
522 546
547 + function getCleanFile() {
548 + return {
549 + page_id: pageId
550 + };
551 + }
523 552
553 + // Angular-UI-Sort options
554 + $scope.sortOptions = {
555 + handle: '.handle',
556 + items: '> tr',
557 + containment: "parent",
558 + axis: "y",
559 + stop: sortUpdate,
560 + };
524 561
562 + /**
563 + * Event listener for sort changes.
564 + * Updates the file ordering on the server.
565 + * @param event
566 + * @param ui
567 + */
568 + function sortUpdate(event, ui) {
569 + let newOrder = $scope.files.map(file => {return file.id}).join(':');
570 + if (newOrder === currentOrder) return;
571 +
572 + currentOrder = newOrder;
573 + $http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
574 + events.emit('success', resp.data.message);
575 + }, checkError('sort'));
576 + }
525 577
578 + /**
579 + * Used by dropzone to get the endpoint to upload to.
580 + * @returns {string}
581 + */
582 + $scope.getUploadUrl = function (file) {
583 + let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
584 + return window.baseUrl(`/attachments/upload${suffix}`);
585 + };
526 586
587 + /**
588 + * Get files for the current page from the server.
589 + */
590 + function getFiles() {
591 + let url = window.baseUrl(`/attachments/get/page/${pageId}`)
592 + $http.get(url).then(resp => {
593 + $scope.files = resp.data;
594 + currentOrder = resp.data.map(file => {return file.id}).join(':');
595 + }, checkError('get'));
596 + }
597 + getFiles();
527 598
599 + /**
600 + * Runs on file upload, Adds an file to local file list
601 + * and shows a success message to the user.
602 + * @param file
603 + * @param data
604 + */
605 + $scope.uploadSuccess = function (file, data) {
606 + $scope.$apply(() => {
607 + $scope.files.push(data);
608 + });
609 + events.emit('success', 'File uploaded');
610 + };
528 611
612 + /**
613 + * Upload and overwrite an existing file.
614 + * @param file
615 + * @param data
616 + */
617 + $scope.uploadSuccessUpdate = function (file, data) {
618 + $scope.$apply(() => {
619 + let search = filesIndexOf(data);
620 + if (search !== -1) $scope.files[search] = data;
529 621
622 + if ($scope.editFile) {
623 + $scope.editFile = angular.copy(data);
624 + data.link = '';
625 + }
626 + });
627 + events.emit('success', 'File updated');
628 + };
530 629
630 + /**
631 + * Delete a file from the server and, on success, the local listing.
632 + * @param file
633 + */
634 + $scope.deleteFile = function(file) {
635 + if (!file.deleting) {
636 + file.deleting = true;
637 + return;
638 + }
639 + $http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
640 + events.emit('success', resp.data.message);
641 + $scope.files.splice($scope.files.indexOf(file), 1);
642 + }, checkError('delete'));
643 + };
531 644
645 + /**
646 + * Attach a link to a page.
647 + * @param file
648 + */
649 + $scope.attachLinkSubmit = function(file) {
650 + file.uploaded_to = pageId;
651 + $http.post(window.baseUrl('/attachments/link'), file).then(resp => {
652 + $scope.files.push(resp.data);
653 + events.emit('success', 'Link attached');
654 + $scope.file = getCleanFile();
655 + }, checkError('link'));
656 + };
532 657
658 + /**
659 + * Start the edit mode for a file.
660 + * @param file
661 + */
662 + $scope.startEdit = function(file) {
663 + $scope.editFile = angular.copy(file);
664 + $scope.editFile.link = (file.external) ? file.path : '';
665 + };
533 666
667 + /**
668 + * Cancel edit mode
669 + */
670 + $scope.cancelEdit = function() {
671 + $scope.editFile = false;
672 + };
534 673
674 + /**
675 + * Update the name and link of a file.
676 + * @param file
677 + */
678 + $scope.updateFile = function(file) {
679 + $http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
680 + let search = filesIndexOf(resp.data);
681 + if (search !== -1) $scope.files[search] = resp.data;
535 682
683 + if ($scope.editFile && !file.external) {
684 + $scope.editFile.link = '';
685 + }
686 + $scope.editFile = false;
687 + events.emit('success', 'Attachment details updated');
688 + }, checkError('edit'));
689 + };
536 690
691 + /**
692 + * Get the url of a file.
693 + */
694 + $scope.getFileUrl = function(file) {
695 + return window.baseUrl('/attachments/' + file.id);
696 + };
697 +
698 + /**
699 + * Search the local files via another file object.
700 + * Used to search via object copies.
701 + * @param file
702 + * @returns int
703 + */
704 + function filesIndexOf(file) {
705 + for (let i = 0; i < $scope.files.length; i++) {
706 + if ($scope.files[i].id == file.id) return i;
707 + }
708 + return -1;
709 + }
710 +
711 + /**
712 + * Check for an error response in a ajax request.
713 + * @param errorGroupName
714 + */
715 + function checkError(errorGroupName) {
716 + $scope.errors[errorGroupName] = {};
717 + return function(response) {
718 + if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
719 + events.emit('error', response.data.error);
720 + }
721 + if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
722 + $scope.errors[errorGroupName] = response.data.validation;
723 + console.log($scope.errors[errorGroupName])
724 + }
725 + }
726 + }
727 +
728 + }]);
729 +
730 +};
......
...@@ -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 }
...@@ -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
......
...@@ -52,3 +52,13 @@ table.list-table { ...@@ -52,3 +52,13 @@ table.list-table {
52 padding: $-xs; 52 padding: $-xs;
53 } 53 }
54 } 54 }
55 +
56 +table.file-table {
57 + @extend .no-style;
58 + td {
59 + padding: $-xs;
60 + }
61 + .ui-sortable-helper {
62 + display: table;
63 + }
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 + @if (setting('app-name-header'))
42 <span class="logo-text">{{ setting('app-name') }}</span> 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
......
1 -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3 -<html xmlns="http://www.w3.org/1999/xhtml"
4 - style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
5 -
6 -<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
7 - <meta name="viewport" content="width=device-width"
8 - style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
9 - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
10 - style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
11 - <title>Confirm Your Email At {{ setting('app-name')}}</title>
12 - <style style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
13 - * {
14 - margin: 0;
15 - padding: 0;
16 - font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
17 - font-size: 100%;
18 - line-height: 1.6;
19 - }
20 -
21 - img {
22 - max-width: 100%;
23 - }
24 -
25 - body {
26 - -webkit-font-smoothing: antialiased;
27 - -webkit-text-size-adjust: none;
28 - width: 100% !important;
29 - height: 100%;
30 - }
31 -
32 - a {
33 - color: #348eda;
34 - }
35 -
36 - .btn-primary {
37 - text-decoration: none;
38 - color: #FFF;
39 - background-color: #348eda;
40 - border: solid #348eda;
41 - border-width: 10px 20px;
42 - line-height: 2;
43 - font-weight: bold;
44 - margin-right: 10px;
45 - text-align: center;
46 - cursor: pointer;
47 - display: inline-block;
48 - border-radius: 4px;
49 - }
50 -
51 - .btn-secondary {
52 - text-decoration: none;
53 - color: #FFF;
54 - background-color: #aaa;
55 - border: solid #aaa;
56 - border-width: 10px 20px;
57 - line-height: 2;
58 - font-weight: bold;
59 - margin-right: 10px;
60 - text-align: center;
61 - cursor: pointer;
62 - display: inline-block;
63 - border-radius: 25px;
64 - }
65 -
66 - .last {
67 - margin-bottom: 0;
68 - }
69 -
70 - .first {
71 - margin-top: 0;
72 - }
73 -
74 - .padding {
75 - padding: 10px 0;
76 - }
77 -
78 - table.body-wrap {
79 - width: 100%;
80 - padding: 20px;
81 - }
82 -
83 - table.body-wrap .container {
84 - border: 1px solid #f0f0f0;
85 - }
86 -
87 - h1,
88 - h2,
89 - h3 {
90 - font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
91 - color: #444;
92 - margin: 10px 0 10px;
93 - line-height: 1.2;
94 - font-weight: 200;
95 - }
96 -
97 - h1 {
98 - font-size: 36px;
99 - }
100 -
101 - h2 {
102 - font-size: 28px;
103 - }
104 -
105 - h3 {
106 - font-size: 22px;
107 - }
108 -
109 - p,
110 - ul,
111 - ol {
112 - margin-bottom: 10px;
113 - font-weight: normal;
114 - font-size: 14px;
115 - color: #888888;
116 - }
117 -
118 - ul li,
119 - ol li {
120 - margin-left: 5px;
121 - list-style-position: inside;
122 - }
123 -
124 - .container {
125 - display: block !important;
126 - max-width: 600px !important;
127 - margin: 0 auto !important;
128 - clear: both !important;
129 - }
130 -
131 - .body-wrap .container {
132 - padding: 20px;
133 - }
134 -
135 - .content {
136 - max-width: 600px;
137 - margin: 0 auto;
138 - display: block;
139 - }
140 -
141 - .content table {
142 - width: 100%;
143 - }
144 - </style>
145 -</head>
146 -
147 -<body bgcolor="#f6f6f6"
148 - style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;">
149 -<!-- body -->
150 -<table class="body-wrap" bgcolor="#f6f6f6"
151 - style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;">
152 - <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
153 - <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
154 - <td class="container" bgcolor="#FFFFFF"
155 - style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;display:block!important;max-width:600px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;border-width:1px;border-style:solid;border-color:#f0f0f0;">
156 - <!-- content -->
157 - <div class="content"
158 - style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;">
159 - <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
160 - <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
161 - <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
162 - <h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;color:#444;margin-top:10px;margin-bottom:10px;margin-right:0;margin-left:0;line-height:1.2;font-weight:200;font-size:36px;">
163 - Email Confirmation</h1>
164 - <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
165 - Thanks for joining <a href="{{ baseUrl('/', true) }}">{{ setting('app-name')}}</a>. <br/>
166 - Please confirm your email address by clicking the button below.</p>
167 - <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
168 - <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
169 - <td class="padding"
170 - style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;padding-top:10px;padding-bottom:10px;padding-right:0;padding-left:0;">
171 - <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
172 - <a class="btn-primary" href="{{ baseUrl('/register/confirm/' . $token, true) }}"
173 - style="margin-top:0;margin-bottom:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;text-decoration:none;color:#FFF;background-color:#348eda;border-style:solid;border-color:#348eda;border-width:10px 20px;line-height:2;font-weight:bold;margin-right:10px;text-align:center;cursor:pointer;display:inline-block;border-radius:4px;">Confirm
174 - Email</a></p>
175 - </td>
176 - </tr>
177 - </table>
178 - </td>
179 - </tr>
180 - </table>
181 - </div>
182 - <!-- /content -->
183 - </td>
184 - <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
185 - </tr>
186 -</table>
187 -<!-- /body -->
188 -</body>
189 -
190 -</html>
1 -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> <head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/> <title>Password Reset From {{ setting('app-name')}}</title> <style style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> * { margin: 0; padding: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; } img { max-width: 100%; } body { -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; } a { color: #348eda; } .btn-primary { text-decoration: none; color: #FFF; background-color: #348eda; border: solid #348eda; border-width: 10px 20px; line-height: 2; font-weight: bold; margin-right: 10px; text-align: center; cursor: pointer; display: inline-block; border-radius: 4px; } .btn-secondary { text-decoration: none; color: #FFF; background-color: #aaa; border: solid #aaa; border-width: 10px 20px; line-height: 2; font-weight: bold; margin-right: 10px; text-align: center; cursor: pointer; display: inline-block; border-radius: 25px; } .last { margin-bottom: 0; } .first { margin-top: 0; } .padding { padding: 10px 0; } table.body-wrap { width: 100%; padding: 20px; } table.body-wrap .container { border: 1px solid #f0f0f0; } h1, h2, h3 { font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; color: #444; margin: 10px 0 10px; line-height: 1.2; font-weight: 200; } h1 { font-size: 36px; } h2 { font-size: 28px; } h3 { font-size: 22px; } p, ul, ol { margin-bottom: 10px; font-weight: normal; font-size: 14px; color: #888888; } ul li, ol li { margin-left: 5px; list-style-position: inside; } .container { display: block !important; max-width: 600px !important; margin: 0 auto !important; clear: both !important; } .body-wrap .container { padding: 20px; } .content { max-width: 600px; margin: 0 auto; display: block; } .content table { width: 100%; } </style></head> <body bgcolor="#f6f6f6" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;"><!-- body --><table class="body-wrap" bgcolor="#f6f6f6" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;"> <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td> <td class="container" bgcolor="#FFFFFF" style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;display:block!important;max-width:600px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;border-width:1px;border-style:solid;border-color:#f0f0f0;"> <!-- content --> <div class="content" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;"> <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;"> <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> <h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;color:#444;margin-top:10px;margin-bottom:10px;margin-right:0;margin-left:0;line-height:1.2;font-weight:200;font-size:36px;"> Password Reset</h1> <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;"> A password reset was requested for this email address on <a href="{{ baseUrl('/', true) }}">{{ setting('app-name')}}</a>. If you did not request a password change please ignore this email.</p> <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;"> <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"> <td class="padding" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;padding-top:10px;padding-bottom:10px;padding-right:0;padding-left:0;"> <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;"> <a class="btn-primary" href="{{ baseUrl('/password/reset/' . $token, true) }}" style="margin-top:0;margin-bottom:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;text-decoration:none;color:#FFF;background-color:#348eda;border-style:solid;border-color:#348eda;border-width:10px 20px;line-height:2;font-weight:bold;margin-right:10px;text-align:center;cursor:pointer;display:inline-block;border-radius:4px;">Click here to reset your password</a></p> </td> </tr> </table> </td> </tr> </table> </div> <!-- /content --> </td> <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td> </tr></table><!-- /body --></body> </html>
...\ No newline at end of file ...\ No newline at end of file
...@@ -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 + @if (isset($diff) && $diff)
28 + {!! $diff !!}
29 + @else
27 {!! $page->html !!} 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)
53 <td> 52 <td>
54 - <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a> 53 + <a href="{{ $revision->getUrl('changes') }}" target="_blank">Changes</a>
55 <span class="text-muted">&nbsp;|&nbsp;</span> 54 <span class="text-muted">&nbsp;|&nbsp;</span>
56 - <a href="{{ $revision->getUrl() }}/restore">Restore</a> 55 +
57 - </td> 56 + @if ($index === 0)
57 + <a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a>
58 @else 58 @else
59 - <td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td> 59 + <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
60 + <span class="text-muted">&nbsp;|&nbsp;</span>
61 + <a href="{{ $revision->getUrl('restore') }}" target="_blank">Restore</a>
60 @endif 62 @endif
63 + </td>
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>
......
1 1
2 <div class="book-tree" ng-non-bindable> 2 <div class="book-tree" ng-non-bindable>
3 +
4 + @if (isset($page) && $page->attachments->count() > 0)
5 + <h6 class="text-muted">Attachments</h6>
6 + @foreach($page->attachments as $attachment)
7 + <div class="attachment">
8 + <a href="{{ $attachment->getUrl() }}" @if($attachment->external) target="_blank" @endif><i class="zmdi zmdi-{{ $attachment->external ? 'open-in-new' : 'file' }}"></i> {{ $attachment->name }}</a>
9 + </div>
10 + @endforeach
11 + @endif
12 +
13 + @if (isset($pageNav) && $pageNav)
14 + <h6 class="text-muted">Page Navigation</h6>
15 + <div class="sidebar-page-nav menu">
16 + @foreach($pageNav as $navItem)
17 + <li class="page-nav-item {{ $navItem['nodeName'] }}">
18 + <a href="{{ $navItem['link'] }}">{{ $navItem['text'] }}</a>
19 + </li>
20 + @endforeach
21 + </div>
22 + @endif
23 +
3 <h6 class="text-muted">Book Navigation</h6> 24 <h6 class="text-muted">Book Navigation</h6>
4 <ul class="sidebar-page-list menu"> 25 <ul class="sidebar-page-list menu">
5 <li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li> 26 <li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
14 .nav-tabs a.selected, .nav-tabs .tab-item.selected { 14 .nav-tabs a.selected, .nav-tabs .tab-item.selected {
15 border-bottom-color: {{ setting('app-color') }}; 15 border-bottom-color: {{ setting('app-color') }};
16 } 16 }
17 - p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus { 17 + .text-primary, p.primary, p .primary, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
18 color: {{ setting('app-color') }}; 18 color: {{ setting('app-color') }};
19 } 19 }
20 </style> 20 </style>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -36,7 +36,9 @@ ...@@ -36,7 +36,9 @@
36 @if(setting('app-logo', '') !== 'none') 36 @if(setting('app-logo', '') !== 'none')
37 <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo"> 37 <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
38 @endif 38 @endif
39 + @if (setting('app-name-header'))
39 <span class="logo-text">{{ setting('app-name') }}</span> 40 <span class="logo-text">{{ setting('app-name') }}</span>
41 + @endif
40 </a> 42 </a>
41 </div> 43 </div>
42 <div class="col-md-6"> 44 <div class="col-md-6">
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 6
7 <div class="container small settings-container"> 7 <div class="container small settings-container">
8 8
9 - <h1>Settings</h1> 9 + <h1>{{ trans('settings.settings') }}</h1>
10 10
11 <form action="{{ baseUrl("/settings") }}" method="POST" ng-cloak> 11 <form action="{{ baseUrl("/settings") }}" method="POST" ng-cloak>
12 {!! csrf_field() !!} 12 {!! csrf_field() !!}
...@@ -14,63 +14,72 @@ ...@@ -14,63 +14,72 @@
14 <h3>App Settings</h3> 14 <h3>App Settings</h3>
15 15
16 <div class="row"> 16 <div class="row">
17 +
17 <div class="col-md-6"> 18 <div class="col-md-6">
18 <div class="form-group"> 19 <div class="form-group">
19 - <label for="setting-app-name">Application name</label> 20 + <label for="setting-app-name">{{ trans('settings.app_name') }}</label>
21 + <p class="small">{{ trans('settings.app_name_desc') }}</p>
20 <input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name"> 22 <input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
21 </div> 23 </div>
22 <div class="form-group"> 24 <div class="form-group">
23 - <label>Allow public viewing?</label> 25 + <label>{{ trans('settings.app_name_header') }}</label>
26 + <div toggle-switch name="setting-app-name-header" value="{{ setting('app-name-header') }}"></div>
27 + </div>
28 + <div class="form-group">
29 + <label for="setting-app-public">{{ trans('settings.app_public_viewing') }}</label>
24 <div toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></div> 30 <div toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></div>
25 </div> 31 </div>
26 <div class="form-group"> 32 <div class="form-group">
27 - <label>Enable higher security image uploads?</label> 33 + <label>{{ trans('settings.app_secure_images') }}</label>
28 - <p class="small">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.</p> 34 + <p class="small">{{ trans('settings.app_secure_images_desc') }}</p>
29 <div toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></div> 35 <div toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></div>
30 </div> 36 </div>
31 <div class="form-group"> 37 <div class="form-group">
32 - <label for="setting-app-editor">Page editor</label> 38 + <label for="setting-app-editor">{{ trans('settings.app_editor') }}</label>
33 - <p class="small">Select which editor will be used by all users to edit pages.</p> 39 + <p class="small">{{ trans('settings.app_editor_desc') }}</p>
34 <select name="setting-app-editor" id="setting-app-editor"> 40 <select name="setting-app-editor" id="setting-app-editor">
35 <option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option> 41 <option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
36 <option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option> 42 <option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
37 </select> 43 </select>
38 </div> 44 </div>
39 </div> 45 </div>
46 +
40 <div class="col-md-6"> 47 <div class="col-md-6">
41 <div class="form-group" id="logo-control"> 48 <div class="form-group" id="logo-control">
42 - <label for="setting-app-logo">Application logo</label> 49 + <label for="setting-app-logo">{{ trans('settings.app_logo') }}</label>
43 - <p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p> 50 + <p class="small">{!! trans('settings.app_logo_desc') !!}</p>
44 <image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ setting('app-logo', '') }}" default-image="{{ baseUrl('/logo.png') }}" name="setting-app-logo" image-class="logo-image"></image-picker> 51 <image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ setting('app-logo', '') }}" default-image="{{ baseUrl('/logo.png') }}" name="setting-app-logo" image-class="logo-image"></image-picker>
45 </div> 52 </div>
46 <div class="form-group" id="color-control"> 53 <div class="form-group" id="color-control">
47 - <label for="setting-app-color">Application primary color</label> 54 + <label for="setting-app-color">{{ trans('settings.app_primary_color') }}</label>
48 - <p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p> 55 + <p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
49 <input type="text" value="{{ setting('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1"> 56 <input type="text" value="{{ setting('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
50 <input type="hidden" value="{{ setting('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)"> 57 <input type="hidden" value="{{ setting('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)">
51 </div> 58 </div>
52 </div> 59 </div>
60 +
53 </div> 61 </div>
62 +
54 <div class="form-group"> 63 <div class="form-group">
55 - <label for="setting-app-custom-head">Custom HTML head content</label> 64 + <label for="setting-app-custom-head">{{ trans('settings.app_custom_html') }}</label>
56 - <p class="small">Any content added here will be inserted into the bottom of the &lt;head&gt; section of every page. This is handy for overriding styles or adding analytics code.</p> 65 + <p class="small">{{ trans('settings.app_custom_html_desc') }}</p>
57 <textarea name="setting-app-custom-head" id="setting-app-custom-head">{{ setting('app-custom-head', '') }}</textarea> 66 <textarea name="setting-app-custom-head" id="setting-app-custom-head">{{ setting('app-custom-head', '') }}</textarea>
58 </div> 67 </div>
59 68
60 <hr class="margin-top"> 69 <hr class="margin-top">
61 70
62 - <h3>Registration Settings</h3> 71 + <h3>{{ trans('settings.reg_settings') }}</h3>
63 72
64 <div class="row"> 73 <div class="row">
65 <div class="col-md-6"> 74 <div class="col-md-6">
66 <div class="form-group"> 75 <div class="form-group">
67 - <label for="setting-registration-enabled">Allow registration?</label> 76 + <label for="setting-registration-enabled">{{ trans('settings.reg_allow') }}</label>
68 <div toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></div> 77 <div toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></div>
69 </div> 78 </div>
70 <div class="form-group"> 79 <div class="form-group">
71 - <label for="setting-registration-role">Default user role after registration</label> 80 + <label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
72 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif> 81 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
73 - @foreach(\BookStack\Role::visible() as $role) 82 + @foreach(\BookStack\Role::all() as $role)
74 <option value="{{$role->id}}" data-role-name="{{ $role->name }}" 83 <option value="{{$role->id}}" data-role-name="{{ $role->name }}"
75 @if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif 84 @if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
76 > 85 >
...@@ -80,17 +89,16 @@ ...@@ -80,17 +89,16 @@
80 </select> 89 </select>
81 </div> 90 </div>
82 <div class="form-group"> 91 <div class="form-group">
83 - <label for="setting-registration-confirmation">Require email confirmation?</label> 92 + <label for="setting-registration-confirmation">{{ trans('settings.reg_confirm_email') }}</label>
84 - <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p> 93 + <p class="small">{{ trans('settings.reg_confirm_email_desc') }}</p>
85 <div toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></div> 94 <div toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></div>
86 </div> 95 </div>
87 </div> 96 </div>
88 <div class="col-md-6"> 97 <div class="col-md-6">
89 <div class="form-group"> 98 <div class="form-group">
90 - <label for="setting-registration-restrict">Restrict registration to domain</label> 99 + <label for="setting-registration-restrict">{{ trans('settings.reg_confirm_restrict_domain') }}</label>
91 - <p class="small">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. 100 + <p class="small">{!! trans('settings.reg_confirm_restrict_domain_desc') !!}</p>
92 - <br> Note that users will be able to change their email addresses after successful registration.</p> 101 + <input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="{{ trans('settings.reg_confirm_restrict_domain_placeholder') }}" value="{{ setting('registration-restrict', '') }}">
93 - <input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ setting('registration-restrict', '') }}">
94 </div> 102 </div>
95 </div> 103 </div>
96 </div> 104 </div>
...@@ -101,7 +109,7 @@ ...@@ -101,7 +109,7 @@
101 <span class="float right muted"> 109 <span class="float right muted">
102 BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }} 110 BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
103 </span> 111 </span>
104 - <button type="submit" class="button pos">Save Settings</button> 112 + <button type="submit" class="button pos">{{ trans('settings.settings_save') }}</button>
105 </div> 113 </div>
106 </form> 114 </form>
107 115
......
...@@ -106,6 +106,19 @@ ...@@ -106,6 +106,19 @@
106 <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label> 106 <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
107 </td> 107 </td>
108 </tr> 108 </tr>
109 + <tr>
110 + <td>Attachments</td>
111 + <td>@include('settings/roles/checkbox', ['permission' => 'attachment-create-all'])</td>
112 + <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
113 + <td>
114 + <label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-own']) Own</label>
115 + <label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-all']) All</label>
116 + </td>
117 + <td>
118 + <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-own']) Own</label>
119 + <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-all']) All</label>
120 + </td>
121 + </tr>
109 </table> 122 </table>
110 </div> 123 </div>
111 </div> 124 </div>
......
...@@ -15,7 +15,9 @@ ...@@ -15,7 +15,9 @@
15 </div> 15 </div>
16 <div class="col-sm-4"> 16 <div class="col-sm-4">
17 <p></p> 17 <p></p>
18 + @if($authMethod !== 'system')
18 <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a> 19 <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a>
20 + @endif
19 </div> 21 </div>
20 </div> 22 </div>
21 <div class="row"> 23 <div class="row">
......
1 +@if($user->system_name == 'public')
2 + <p>This user represents any guest users that visit your instance. It cannot be used for logins but is assigned&nbsp;automatically.</p>
3 +@endif
4 +
5 +<div class="form-group">
6 + <label for="name">Name</label>
7 + @include('form.text', ['name' => 'name'])
8 +</div>
9 +
10 +<div class="form-group">
11 + <label for="email">Email</label>
12 + @include('form.text', ['name' => 'email'])
13 +</div>
14 +
15 +@if(userCan('users-manage'))
16 + <div class="form-group">
17 + <label for="role">User Role</label>
18 + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
19 + </div>
20 +@endif
21 +
22 +<div class="form-group">
23 + <a href="{{ baseUrl("/settings/users") }}" class="button muted">Cancel</a>
24 + <button class="button pos" type="submit">Save</button>
25 +</div>
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
22 <div class="row"> 22 <div class="row">
23 <div class="col-sm-8"> 23 <div class="col-sm-8">
24 <div class="compact"> 24 <div class="compact">
25 - {!! $users->links() !!} 25 + {{ $users->links() }}
26 </div> 26 </div>
27 </div> 27 </div>
28 <div class="col-sm-4"> 28 <div class="col-sm-4">
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
76 </table> 76 </table>
77 77
78 <div> 78 <div>
79 - {!! $users->links() !!} 79 + {{ $users->links() }}
80 </div> 80 </div>
81 </div> 81 </div>
82 82
......
1 +<?php
2 +
3 +if (! empty($greeting)) {
4 + echo $greeting, "\n\n";
5 +} else {
6 + echo $level == 'error' ? 'Whoops!' : 'Hello!', "\n\n";
7 +}
8 +
9 +if (! empty($introLines)) {
10 + echo implode("\n", $introLines), "\n\n";
11 +}
12 +
13 +if (isset($actionText)) {
14 + echo "{$actionText}: {$actionUrl}", "\n\n";
15 +}
16 +
17 +if (! empty($outroLines)) {
18 + echo implode("\n", $outroLines), "\n\n";
19 +}
20 +
21 +echo 'Regards,', "\n";
22 +echo config('app.name'), "\n";
1 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 +<html>
3 +<head>
4 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5 + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
6 +
7 + <style type="text/css" rel="stylesheet" media="all">
8 + /* Media Queries */
9 + @media only screen and (max-width: 500px) {
10 + .button {
11 + width: 100% !important;
12 + }
13 + }
14 +
15 + @media only screen and (max-width: 600px) {
16 + .button {
17 + width: 100% !important;
18 + }
19 + .mobile {
20 + max-width: 100%;
21 + display: block;
22 + width: 100%;
23 + }
24 + }
25 + </style>
26 +</head>
27 +
28 +<?php
29 +
30 +$style = [
31 + /* Layout ------------------------------ */
32 +
33 + 'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;',
34 + 'email-wrapper' => 'width: 100%; margin: 0; padding: 0; background-color: #F2F4F6;',
35 +
36 + /* Masthead ----------------------- */
37 +
38 + 'email-masthead' => 'padding: 25px 0; text-align: center;',
39 + 'email-masthead_name' => 'font-size: 24px; font-weight: 400; color: #2F3133; text-decoration: none; text-shadow: 0 1px 0 white;',
40 +
41 + 'email-body' => 'width: 100%; margin: 0; padding: 0; border-top: 4px solid '.setting('app-color').'; border-bottom: 1px solid #EDEFF2; background-color: #FFF;',
42 + 'email-body_inner' => 'width: auto; max-width: 100%; margin: 0 auto; padding: 0;',
43 + 'email-body_cell' => 'padding: 35px;',
44 +
45 + 'email-footer' => 'width: auto; max-width: 570px; margin: 0 auto; padding: 0; text-align: center;',
46 + 'email-footer_cell' => 'color: #AEAEAE; padding: 35px; text-align: center;',
47 +
48 + /* Body ------------------------------ */
49 +
50 + 'body_action' => 'width: 100%; margin: 30px auto; padding: 0; text-align: center;',
51 + 'body_sub' => 'margin-top: 25px; padding-top: 25px; border-top: 1px solid #EDEFF2;',
52 +
53 + /* Type ------------------------------ */
54 +
55 + 'anchor' => 'color: '.setting('app-color').';overflow-wrap: break-word;word-wrap: break-word;word-break: break-all;word-break:break-word;',
56 + 'header-1' => 'margin-top: 0; color: #2F3133; font-size: 19px; font-weight: bold; text-align: left;',
57 + 'paragraph' => 'margin-top: 0; color: #74787E; font-size: 16px; line-height: 1.5em;',
58 + 'paragraph-sub' => 'margin-top: 0; color: #74787E; font-size: 12px; line-height: 1.5em;',
59 + 'paragraph-center' => 'text-align: center;',
60 +
61 + /* Buttons ------------------------------ */
62 +
63 + 'button' => 'display: block; display: inline-block; width: 200px; min-height: 20px; padding: 10px;
64 + background-color: #3869D4; border-radius: 3px; color: #ffffff; font-size: 15px; line-height: 25px;
65 + text-align: center; text-decoration: none; -webkit-text-size-adjust: none;',
66 +
67 + 'button--green' => 'background-color: #22BC66;',
68 + 'button--red' => 'background-color: #dc4d2f;',
69 + 'button--blue' => 'background-color: '.setting('app-color').';',
70 +];
71 +?>
72 +
73 +<?php $fontFamily = 'font-family: Arial, \'Helvetica Neue\', Helvetica, sans-serif;'; ?>
74 +
75 +<body style="{{ $style['body'] }}">
76 + <table width="100%" cellpadding="0" cellspacing="0">
77 + <tr>
78 + <td align="center" class="mobile">
79 + <table width="600" style="max-width: 100%; padding: 12px;text-align: left;" cellpadding="0" cellspacing="0" class="mobile">
80 + <tr>
81 + <td style="{{ $style['email-wrapper'] }}" align="center">
82 + <table width="100%" cellpadding="0" cellspacing="0">
83 + <!-- Logo -->
84 + <tr>
85 + <td style="{{ $style['email-masthead'] }}">
86 + <a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ baseUrl('/') }}" target="_blank">
87 + {{ setting('app-name') }}
88 + </a>
89 + </td>
90 + </tr>
91 +
92 + <!-- Email Body -->
93 + <tr>
94 + <td style="{{ $style['email-body'] }}" width="100%">
95 + <table style="{{ $style['email-body_inner'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
96 + <tr>
97 + <td style="{{ $fontFamily }} {{ $style['email-body_cell'] }}">
98 +
99 + <!-- Greeting -->
100 + @if (!empty($greeting) || $level == 'error')
101 + <h1 style="{{ $style['header-1'] }}">
102 + @if (! empty($greeting))
103 + {{ $greeting }}
104 + @else
105 + @if ($level == 'error')
106 + Whoops!
107 + @endif
108 + @endif
109 + </h1>
110 + @endif
111 +
112 + <!-- Intro -->
113 + @foreach ($introLines as $line)
114 + <p style="{{ $style['paragraph'] }}">
115 + {{ $line }}
116 + </p>
117 + @endforeach
118 +
119 + <!-- Action Button -->
120 + @if (isset($actionText))
121 + <table style="{{ $style['body_action'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
122 + <tr>
123 + <td align="center">
124 + <?php
125 + switch ($level) {
126 + case 'success':
127 + $actionColor = 'button--green';
128 + break;
129 + case 'error':
130 + $actionColor = 'button--red';
131 + break;
132 + default:
133 + $actionColor = 'button--blue';
134 + }
135 + ?>
136 +
137 + <a href="{{ $actionUrl }}"
138 + style="{{ $fontFamily }} {{ $style['button'] }} {{ $style[$actionColor] }}"
139 + class="button"
140 + target="_blank">
141 + {{ $actionText }}
142 + </a>
143 + </td>
144 + </tr>
145 + </table>
146 + @endif
147 +
148 + <!-- Outro -->
149 + @foreach ($outroLines as $line)
150 + <p style="{{ $style['paragraph'] }}">
151 + {{ $line }}
152 + </p>
153 + @endforeach
154 +
155 +
156 + <!-- Sub Copy -->
157 + @if (isset($actionText))
158 + <table style="{{ $style['body_sub'] }}">
159 + <tr>
160 + <td style="{{ $fontFamily }}">
161 + <p style="{{ $style['paragraph-sub'] }}">
162 + If you’re having trouble clicking the "{{ $actionText }}" button,
163 + copy and paste the URL below into your web browser:
164 + </p>
165 +
166 + <p style="{{ $style['paragraph-sub'] }}">
167 + <a style="{{ $style['anchor'] }}" href="{{ $actionUrl }}" target="_blank">
168 + {{ $actionUrl }}
169 + </a>
170 + </p>
171 + </td>
172 + </tr>
173 + </table>
174 + @endif
175 +
176 + </td>
177 + </tr>
178 + </table>
179 + </td>
180 + </tr>
181 +
182 + <!-- Footer -->
183 + <tr>
184 + <td>
185 + <table style="{{ $style['email-footer'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
186 + <tr>
187 + <td style="{{ $fontFamily }} {{ $style['email-footer_cell'] }}">
188 + <p style="{{ $style['paragraph-sub'] }}">
189 + &copy; {{ date('Y') }}
190 + <a style="{{ $style['anchor'] }}" href="{{ baseUrl('/') }}" target="_blank">{{ setting('app-name') }}</a>.
191 + All rights reserved.
192 + </p>
193 + </td>
194 + </tr>
195 + </table>
196 + </td>
197 + </tr>
198 + </table>
199 + </td>
200 + </tr>
201 + </table>
202 + </td>
203 + </tr>
204 + </table>
205 +</body>
206 +</html>
...@@ -27,6 +27,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -27,6 +27,7 @@ Route::group(['middleware' => 'auth'], function () {
27 27
28 // Pages 28 // Pages
29 Route::get('/{bookSlug}/page/create', 'PageController@create'); 29 Route::get('/{bookSlug}/page/create', 'PageController@create');
30 + Route::post('/{bookSlug}/page/create/guest', 'PageController@createAsGuest');
30 Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft'); 31 Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
31 Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store'); 32 Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
32 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 33 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
...@@ -47,10 +48,12 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -47,10 +48,12 @@ Route::group(['middleware' => 'auth'], function () {
47 // Revisions 48 // Revisions
48 Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions'); 49 Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
49 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision'); 50 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
51 + Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
50 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision'); 52 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
51 53
52 // Chapters 54 // Chapters
53 Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create'); 55 Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
56 + Route::post('/{bookSlug}/chapter/{chapterSlug}/page/create/guest', 'PageController@createAsGuest');
54 Route::get('/{bookSlug}/chapter/create', 'ChapterController@create'); 57 Route::get('/{bookSlug}/chapter/create', 'ChapterController@create');
55 Route::post('/{bookSlug}/chapter/create', 'ChapterController@store'); 58 Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
56 Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); 59 Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
...@@ -84,6 +87,16 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -84,6 +87,16 @@ Route::group(['middleware' => 'auth'], function () {
84 Route::delete('/{imageId}', 'ImageController@destroy'); 87 Route::delete('/{imageId}', 'ImageController@destroy');
85 }); 88 });
86 89
90 + // Attachments routes
91 + Route::get('/attachments/{id}', 'AttachmentController@get');
92 + Route::post('/attachments/upload', 'AttachmentController@upload');
93 + Route::post('/attachments/upload/{id}', 'AttachmentController@uploadUpdate');
94 + Route::post('/attachments/link', 'AttachmentController@attachLink');
95 + Route::put('/attachments/{id}', 'AttachmentController@update');
96 + Route::get('/attachments/get/page/{pageId}', 'AttachmentController@listForPage');
97 + Route::put('/attachments/sort/page/{pageId}', 'AttachmentController@sortForPage');
98 + Route::delete('/attachments/{id}', 'AttachmentController@delete');
99 +
87 // AJAX routes 100 // AJAX routes
88 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); 101 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
89 Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); 102 Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
...@@ -139,27 +152,27 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -139,27 +152,27 @@ Route::group(['middleware' => 'auth'], function () {
139 152
140 }); 153 });
141 154
142 -// Login using social authentication 155 +// Social auth routes
143 -Route::get('/login/service/{socialDriver}', 'Auth\AuthController@getSocialLogin'); 156 +Route::get('/login/service/{socialDriver}', 'Auth\RegisterController@getSocialLogin');
144 -Route::get('/login/service/{socialDriver}/callback', 'Auth\AuthController@socialCallback'); 157 +Route::get('/login/service/{socialDriver}/callback', 'Auth\RegisterController@socialCallback');
145 -Route::get('/login/service/{socialDriver}/detach', 'Auth\AuthController@detachSocialAccount'); 158 +Route::get('/login/service/{socialDriver}/detach', 'Auth\RegisterController@detachSocialAccount');
159 +Route::get('/register/service/{socialDriver}', 'Auth\RegisterController@socialRegister');
146 160
147 // Login/Logout routes 161 // Login/Logout routes
148 -Route::get('/login', 'Auth\AuthController@getLogin'); 162 +Route::get('/login', 'Auth\LoginController@getLogin');
149 -Route::post('/login', 'Auth\AuthController@postLogin'); 163 +Route::post('/login', 'Auth\LoginController@login');
150 -Route::get('/logout', 'Auth\AuthController@getLogout'); 164 +Route::get('/logout', 'Auth\LoginController@logout');
151 -Route::get('/register', 'Auth\AuthController@getRegister'); 165 +Route::get('/register', 'Auth\RegisterController@getRegister');
152 -Route::get('/register/confirm', 'Auth\AuthController@getRegisterConfirmation'); 166 +Route::get('/register/confirm', 'Auth\RegisterController@getRegisterConfirmation');
153 -Route::get('/register/confirm/awaiting', 'Auth\AuthController@showAwaitingConfirmation'); 167 +Route::get('/register/confirm/awaiting', 'Auth\RegisterController@showAwaitingConfirmation');
154 -Route::post('/register/confirm/resend', 'Auth\AuthController@resendConfirmation'); 168 +Route::post('/register/confirm/resend', 'Auth\RegisterController@resendConfirmation');
155 -Route::get('/register/confirm/{token}', 'Auth\AuthController@confirmEmail'); 169 +Route::get('/register/confirm/{token}', 'Auth\RegisterController@confirmEmail');
156 -Route::get('/register/confirm/{token}/email', 'Auth\AuthController@viewConfirmEmail'); 170 +Route::post('/register', 'Auth\RegisterController@postRegister');
157 -Route::get('/register/service/{socialDriver}', 'Auth\AuthController@socialRegister');
158 -Route::post('/register', 'Auth\AuthController@postRegister');
159 171
160 // Password reset link request routes... 172 // Password reset link request routes...
161 -Route::get('/password/email', 'Auth\PasswordController@getEmail'); 173 +Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
162 -Route::post('/password/email', 'Auth\PasswordController@postEmail'); 174 +Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
175 +
163 // Password reset routes... 176 // Password reset routes...
164 -Route::get('/password/reset/{token}', 'Auth\PasswordController@getReset');
165 -Route::post('/password/reset', 'Auth\PasswordController@postReset');
...\ No newline at end of file ...\ No newline at end of file
177 +Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
178 +Route::post('/password/reset', 'Auth\ResetPasswordController@reset');
...\ No newline at end of file ...\ No newline at end of file
......
1 +<?php
2 +
3 +class AttachmentTest extends TestCase
4 +{
5 + /**
6 + * Get a test file that can be uploaded
7 + * @param $fileName
8 + * @return \Illuminate\Http\UploadedFile
9 + */
10 + protected function getTestFile($fileName)
11 + {
12 + return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
13 + }
14 +
15 + /**
16 + * Uploads a file with the given name.
17 + * @param $name
18 + * @param int $uploadedTo
19 + * @return string
20 + */
21 + protected function uploadFile($name, $uploadedTo = 0)
22 + {
23 + $file = $this->getTestFile($name);
24 + return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
25 + }
26 +
27 + /**
28 + * Get the expected upload path for a file.
29 + * @param $fileName
30 + * @return string
31 + */
32 + protected function getUploadPath($fileName)
33 + {
34 + return 'uploads/files/' . Date('Y-m-M') . '/' . $fileName;
35 + }
36 +
37 + /**
38 + * Delete all uploaded files.
39 + * To assist with cleanup.
40 + */
41 + protected function deleteUploads()
42 + {
43 + $fileService = $this->app->make(\BookStack\Services\AttachmentService::class);
44 + foreach (\BookStack\Attachment::all() as $file) {
45 + $fileService->deleteFile($file);
46 + }
47 + }
48 +
49 + public function test_file_upload()
50 + {
51 + $page = \BookStack\Page::first();
52 + $this->asAdmin();
53 + $admin = $this->getAdmin();
54 + $fileName = 'upload_test_file.txt';
55 +
56 + $expectedResp = [
57 + 'name' => $fileName,
58 + 'uploaded_to'=> $page->id,
59 + 'extension' => 'txt',
60 + 'order' => 1,
61 + 'created_by' => $admin->id,
62 + 'updated_by' => $admin->id,
63 + 'path' => $this->getUploadPath($fileName)
64 + ];
65 +
66 + $this->uploadFile($fileName, $page->id);
67 + $this->assertResponseOk();
68 + $this->seeJsonContains($expectedResp);
69 + $this->seeInDatabase('attachments', $expectedResp);
70 +
71 + $this->deleteUploads();
72 + }
73 +
74 + public function test_file_display_and_access()
75 + {
76 + $page = \BookStack\Page::first();
77 + $this->asAdmin();
78 + $admin = $this->getAdmin();
79 + $fileName = 'upload_test_file.txt';
80 +
81 + $this->uploadFile($fileName, $page->id);
82 + $this->assertResponseOk();
83 + $this->visit($page->getUrl())
84 + ->seeLink($fileName)
85 + ->click($fileName)
86 + ->see('Hi, This is a test file for testing the upload process.');
87 +
88 + $this->deleteUploads();
89 + }
90 +
91 + public function test_attaching_link_to_page()
92 + {
93 + $page = \BookStack\Page::first();
94 + $admin = $this->getAdmin();
95 + $this->asAdmin();
96 +
97 + $this->call('POST', 'attachments/link', [
98 + 'link' => 'https://example.com',
99 + 'name' => 'Example Attachment Link',
100 + 'uploaded_to' => $page->id,
101 + ]);
102 +
103 + $expectedResp = [
104 + 'path' => 'https://example.com',
105 + 'name' => 'Example Attachment Link',
106 + 'uploaded_to' => $page->id,
107 + 'created_by' => $admin->id,
108 + 'updated_by' => $admin->id,
109 + 'external' => true,
110 + 'order' => 1,
111 + 'extension' => ''
112 + ];
113 +
114 + $this->assertResponseOk();
115 + $this->seeJsonContains($expectedResp);
116 + $this->seeInDatabase('attachments', $expectedResp);
117 +
118 + $this->visit($page->getUrl())->seeLink('Example Attachment Link')
119 + ->click('Example Attachment Link')->seePageIs('https://example.com');
120 +
121 + $this->deleteUploads();
122 + }
123 +
124 + public function test_attachment_updating()
125 + {
126 + $page = \BookStack\Page::first();
127 + $this->asAdmin();
128 +
129 + $this->call('POST', 'attachments/link', [
130 + 'link' => 'https://example.com',
131 + 'name' => 'Example Attachment Link',
132 + 'uploaded_to' => $page->id,
133 + ]);
134 +
135 + $attachmentId = \BookStack\Attachment::first()->id;
136 +
137 + $this->call('PUT', 'attachments/' . $attachmentId, [
138 + 'uploaded_to' => $page->id,
139 + 'name' => 'My new attachment name',
140 + 'link' => 'https://test.example.com'
141 + ]);
142 +
143 + $expectedResp = [
144 + 'path' => 'https://test.example.com',
145 + 'name' => 'My new attachment name',
146 + 'uploaded_to' => $page->id
147 + ];
148 +
149 + $this->assertResponseOk();
150 + $this->seeJsonContains($expectedResp);
151 + $this->seeInDatabase('attachments', $expectedResp);
152 +
153 + $this->deleteUploads();
154 + }
155 +
156 + public function test_file_deletion()
157 + {
158 + $page = \BookStack\Page::first();
159 + $this->asAdmin();
160 + $fileName = 'deletion_test.txt';
161 + $this->uploadFile($fileName, $page->id);
162 +
163 + $filePath = base_path('storage/' . $this->getUploadPath($fileName));
164 +
165 + $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
166 +
167 + $attachmentId = \BookStack\Attachment::first()->id;
168 + $this->call('DELETE', 'attachments/' . $attachmentId);
169 +
170 + $this->dontSeeInDatabase('attachments', [
171 + 'name' => $fileName
172 + ]);
173 + $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
174 +
175 + $this->deleteUploads();
176 + }
177 +
178 + public function test_attachment_deletion_on_page_deletion()
179 + {
180 + $page = \BookStack\Page::first();
181 + $this->asAdmin();
182 + $fileName = 'deletion_test.txt';
183 + $this->uploadFile($fileName, $page->id);
184 +
185 + $filePath = base_path('storage/' . $this->getUploadPath($fileName));
186 +
187 + $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
188 + $this->seeInDatabase('attachments', [
189 + 'name' => $fileName
190 + ]);
191 +
192 + $this->call('DELETE', $page->getUrl());
193 +
194 + $this->dontSeeInDatabase('attachments', [
195 + 'name' => $fileName
196 + ]);
197 + $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
198 +
199 + $this->deleteUploads();
200 + }
201 +}
1 <?php 1 <?php
2 2
3 -use BookStack\EmailConfirmation; 3 +use BookStack\Notifications\ConfirmEmail;
4 +use Illuminate\Support\Facades\Notification;
4 5
5 class AuthTest extends TestCase 6 class AuthTest extends TestCase
6 { 7 {
...@@ -57,15 +58,13 @@ class AuthTest extends TestCase ...@@ -57,15 +58,13 @@ class AuthTest extends TestCase
57 58
58 public function test_confirmed_registration() 59 public function test_confirmed_registration()
59 { 60 {
61 + // Fake notifications
62 + Notification::fake();
63 +
60 // Set settings and get user instance 64 // Set settings and get user instance
61 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); 65 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
62 $user = factory(\BookStack\User::class)->make(); 66 $user = factory(\BookStack\User::class)->make();
63 67
64 - // Mock Mailer to ensure mail is being sent
65 - $mockMailer = Mockery::mock('Illuminate\Contracts\Mail\Mailer');
66 - $mockMailer->shouldReceive('send')->with('emails/email-confirmation', Mockery::type('array'), Mockery::type('callable'))->twice();
67 - $this->app->instance('mailer', $mockMailer);
68 -
69 // Go through registration process 68 // Go through registration process
70 $this->visit('/register') 69 $this->visit('/register')
71 ->see('Sign Up') 70 ->see('Sign Up')
...@@ -76,6 +75,10 @@ class AuthTest extends TestCase ...@@ -76,6 +75,10 @@ class AuthTest extends TestCase
76 ->seePageIs('/register/confirm') 75 ->seePageIs('/register/confirm')
77 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); 76 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
78 77
78 + // Ensure notification sent
79 + $dbUser = \BookStack\User::where('email', '=', $user->email)->first();
80 + Notification::assertSentTo($dbUser, ConfirmEmail::class);
81 +
79 // Test access and resend confirmation email 82 // Test access and resend confirmation email
80 $this->login($user->email, $user->password) 83 $this->login($user->email, $user->password)
81 ->seePageIs('/register/confirm/awaiting') 84 ->seePageIs('/register/confirm/awaiting')
...@@ -84,19 +87,18 @@ class AuthTest extends TestCase ...@@ -84,19 +87,18 @@ class AuthTest extends TestCase
84 ->seePageIs('/register/confirm/awaiting') 87 ->seePageIs('/register/confirm/awaiting')
85 ->press('Resend Confirmation Email'); 88 ->press('Resend Confirmation Email');
86 89
87 - // Get confirmation 90 + // Get confirmation and confirm notification matches
88 - $user = $user->where('email', '=', $user->email)->first(); 91 + $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
89 - $emailConfirmation = EmailConfirmation::where('user_id', '=', $user->id)->first(); 92 + Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) {
90 - 93 + return $notification->token === $emailConfirmation->token;
94 + });
91 95
92 - // Check confirmation email button and confirmation activation. 96 + // Check confirmation email confirmation activation.
93 - $this->visit('/register/confirm/' . $emailConfirmation->token . '/email') 97 + $this->visit('/register/confirm/' . $emailConfirmation->token)
94 - ->see('Email Confirmation')
95 - ->click('Confirm Email')
96 ->seePageIs('/') 98 ->seePageIs('/')
97 ->see($user->name) 99 ->see($user->name)
98 ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token]) 100 ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token])
99 - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]); 101 + ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
100 } 102 }
101 103
102 public function test_restricted_registration() 104 public function test_restricted_registration()
...@@ -144,7 +146,7 @@ class AuthTest extends TestCase ...@@ -144,7 +146,7 @@ class AuthTest extends TestCase
144 146
145 public function test_user_updating() 147 public function test_user_updating()
146 { 148 {
147 - $user = \BookStack\User::all()->last(); 149 + $user = $this->getNormalUser();
148 $password = $user->password; 150 $password = $user->password;
149 $this->asAdmin() 151 $this->asAdmin()
150 ->visit('/settings/users') 152 ->visit('/settings/users')
...@@ -160,7 +162,7 @@ class AuthTest extends TestCase ...@@ -160,7 +162,7 @@ class AuthTest extends TestCase
160 162
161 public function test_user_password_update() 163 public function test_user_password_update()
162 { 164 {
163 - $user = \BookStack\User::all()->last(); 165 + $user = $this->getNormalUser();
164 $userProfilePage = '/settings/users/' . $user->id; 166 $userProfilePage = '/settings/users/' . $user->id;
165 $this->asAdmin() 167 $this->asAdmin()
166 ->visit($userProfilePage) 168 ->visit($userProfilePage)
......
...@@ -108,7 +108,7 @@ class LdapTest extends \TestCase ...@@ -108,7 +108,7 @@ class LdapTest extends \TestCase
108 108
109 public function test_user_edit_form() 109 public function test_user_edit_form()
110 { 110 {
111 - $editUser = User::all()->last(); 111 + $editUser = $this->getNormalUser();
112 $this->asAdmin()->visit('/settings/users/' . $editUser->id) 112 $this->asAdmin()->visit('/settings/users/' . $editUser->id)
113 ->see('Edit User') 113 ->see('Edit User')
114 ->dontSee('Password') 114 ->dontSee('Password')
...@@ -126,7 +126,7 @@ class LdapTest extends \TestCase ...@@ -126,7 +126,7 @@ class LdapTest extends \TestCase
126 126
127 public function test_non_admins_cannot_change_auth_id() 127 public function test_non_admins_cannot_change_auth_id()
128 { 128 {
129 - $testUser = User::all()->last(); 129 + $testUser = $this->getNormalUser();
130 $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id) 130 $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
131 ->dontSee('External Authentication'); 131 ->dontSee('External Authentication');
132 } 132 }
......
...@@ -97,6 +97,39 @@ class EntitySearchTest extends TestCase ...@@ -97,6 +97,39 @@ class EntitySearchTest extends TestCase
97 ->seeStatusCode(200); 97 ->seeStatusCode(200);
98 } 98 }
99 99
100 + public function test_tag_search()
101 + {
102 + $newTags = [
103 + new \BookStack\Tag([
104 + 'name' => 'animal',
105 + 'value' => 'cat'
106 + ]),
107 + new \BookStack\Tag([
108 + 'name' => 'color',
109 + 'value' => 'red'
110 + ])
111 + ];
112 +
113 + $pageA = \BookStack\Page::first();
114 + $pageA->tags()->saveMany($newTags);
115 +
116 + $pageB = \BookStack\Page::all()->last();
117 + $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
118 +
119 + $this->asAdmin()->visit('/search/all?term=%5Banimal%5D')
120 + ->seeLink($pageA->name)
121 + ->seeLink($pageB->name);
122 +
123 + $this->visit('/search/all?term=%5Bcolor%5D')
124 + ->seeLink($pageA->name)
125 + ->dontSeeLink($pageB->name);
126 +
127 + $this->visit('/search/all?term=%5Banimal%3Dcat%5D')
128 + ->seeLink($pageA->name)
129 + ->dontSeeLink($pageB->name);
130 +
131 + }
132 +
100 public function test_ajax_entity_search() 133 public function test_ajax_entity_search()
101 { 134 {
102 $page = \BookStack\Page::all()->last(); 135 $page = \BookStack\Page::all()->last();
......
...@@ -236,8 +236,9 @@ class EntityTest extends TestCase ...@@ -236,8 +236,9 @@ class EntityTest extends TestCase
236 ->type('super test page', '#name') 236 ->type('super test page', '#name')
237 ->press('Save Page') 237 ->press('Save Page')
238 // Check redirect 238 // Check redirect
239 - ->seePageIs($newPageUrl) 239 + ->seePageIs($newPageUrl);
240 - ->visit($pageUrl) 240 +
241 + $this->visit($pageUrl)
241 ->seePageIs($newPageUrl); 242 ->seePageIs($newPageUrl);
242 } 243 }
243 244
......
...@@ -10,7 +10,7 @@ class ImageTest extends TestCase ...@@ -10,7 +10,7 @@ class ImageTest extends TestCase
10 */ 10 */
11 protected function getTestImage($fileName) 11 protected function getTestImage($fileName)
12 { 12 {
13 - return new \Illuminate\Http\UploadedFile(base_path('tests/test-image.jpg'), $fileName, 'image/jpeg', 5238); 13 + return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-image.jpg'), $fileName, 'image/jpeg', 5238);
14 } 14 }
15 15
16 /** 16 /**
...@@ -62,7 +62,7 @@ class ImageTest extends TestCase ...@@ -62,7 +62,7 @@ class ImageTest extends TestCase
62 $this->deleteImage($relPath); 62 $this->deleteImage($relPath);
63 63
64 $this->seeInDatabase('images', [ 64 $this->seeInDatabase('images', [
65 - 'url' => url($relPath), 65 + 'url' => $this->baseUrl . $relPath,
66 'type' => 'gallery', 66 'type' => 'gallery',
67 'uploaded_to' => $page->id, 67 'uploaded_to' => $page->id,
68 'path' => $relPath, 68 'path' => $relPath,
...@@ -86,7 +86,7 @@ class ImageTest extends TestCase ...@@ -86,7 +86,7 @@ class ImageTest extends TestCase
86 $this->assertResponseOk(); 86 $this->assertResponseOk();
87 87
88 $this->dontSeeInDatabase('images', [ 88 $this->dontSeeInDatabase('images', [
89 - 'url' => $relPath, 89 + 'url' => $this->baseUrl . $relPath,
90 'type' => 'gallery' 90 'type' => 'gallery'
91 ]); 91 ]);
92 92
......
...@@ -544,27 +544,38 @@ class RolesTest extends TestCase ...@@ -544,27 +544,38 @@ class RolesTest extends TestCase
544 ->dontSeeInElement('.book-content', $otherPage->name); 544 ->dontSeeInElement('.book-content', $otherPage->name);
545 } 545 }
546 546
547 - public function test_public_role_not_visible_in_user_edit_screen() 547 + public function test_public_role_visible_in_user_edit_screen()
548 { 548 {
549 $user = \BookStack\User::first(); 549 $user = \BookStack\User::first();
550 $this->asAdmin()->visit('/settings/users/' . $user->id) 550 $this->asAdmin()->visit('/settings/users/' . $user->id)
551 ->seeElement('#roles-admin') 551 ->seeElement('#roles-admin')
552 - ->dontSeeElement('#roles-public'); 552 + ->seeElement('#roles-public');
553 } 553 }
554 554
555 - public function test_public_role_not_visible_in_role_listing() 555 + public function test_public_role_visible_in_role_listing()
556 { 556 {
557 $this->asAdmin()->visit('/settings/roles') 557 $this->asAdmin()->visit('/settings/roles')
558 ->see('Admin') 558 ->see('Admin')
559 - ->dontSee('Public'); 559 + ->see('Public');
560 } 560 }
561 561
562 - public function test_public_role_not_visible_in_default_role_setting() 562 + public function test_public_role_visible_in_default_role_setting()
563 { 563 {
564 $this->asAdmin()->visit('/settings') 564 $this->asAdmin()->visit('/settings')
565 ->seeElement('[data-role-name="admin"]') 565 ->seeElement('[data-role-name="admin"]')
566 - ->dontSeeElement('[data-role-name="public"]'); 566 + ->seeElement('[data-role-name="public"]');
567 567
568 } 568 }
569 569
570 + public function test_public_role_not_deleteable()
571 + {
572 + $this->asAdmin()->visit('/settings/roles')
573 + ->click('Public')
574 + ->see('Edit Role')
575 + ->click('Delete Role')
576 + ->press('Confirm')
577 + ->see('Delete Role')
578 + ->see('Cannot be deleted');
579 + }
580 +
570 } 581 }
......
1 <?php 1 <?php
2 2
3 -class PublicViewTest extends TestCase 3 +class PublicActionTest extends TestCase
4 { 4 {
5 5
6 + public function test_app_not_public()
7 + {
8 + $this->setSettings(['app-public' => 'false']);
9 + $book = \BookStack\Book::orderBy('name', 'asc')->first();
10 + $this->visit('/books')->seePageIs('/login');
11 + $this->visit($book->getUrl())->seePageIs('/login');
12 +
13 + $page = \BookStack\Page::first();
14 + $this->visit($page->getUrl())->seePageIs('/login');
15 + }
16 +
6 public function test_books_viewable() 17 public function test_books_viewable()
7 { 18 {
8 $this->setSettings(['app-public' => 'true']); 19 $this->setSettings(['app-public' => 'true']);
...@@ -38,4 +49,35 @@ class PublicViewTest extends TestCase ...@@ -38,4 +49,35 @@ class PublicViewTest extends TestCase
38 ->seePageIs($pageToVisit->getUrl()); 49 ->seePageIs($pageToVisit->getUrl());
39 } 50 }
40 51
52 + public function test_public_page_creation()
53 + {
54 + $this->setSettings(['app-public' => 'true']);
55 + $publicRole = \BookStack\Role::getSystemRole('public');
56 + // Grant all permissions to public
57 + $publicRole->permissions()->detach();
58 + foreach (\BookStack\RolePermission::all() as $perm) {
59 + $publicRole->attachPermission($perm);
60 + }
61 + $this->app[\BookStack\Services\PermissionService::class]->buildJointPermissionForRole($publicRole);
62 +
63 + $chapter = \BookStack\Chapter::first();
64 + $this->visit($chapter->book->getUrl());
65 + $this->visit($chapter->getUrl())
66 + ->click('New Page')
67 + ->see('Create Page')
68 + ->seePageIs($chapter->getUrl('/create-page'));
69 +
70 + $this->submitForm('Continue', [
71 + 'name' => 'My guest page'
72 + ])->seePageIs($chapter->book->getUrl('/page/my-guest-page/edit'));
73 +
74 + $user = \BookStack\User::getDefault();
75 + $this->seeInDatabase('pages', [
76 + 'name' => 'My guest page',
77 + 'chapter_id' => $chapter->id,
78 + 'created_by' => $user->id,
79 + 'updated_by' => $user->id
80 + ]);
81 + }
82 +
41 } 83 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -67,6 +67,14 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -67,6 +67,14 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
67 } 67 }
68 68
69 /** 69 /**
70 + * Get a user that's not a system user such as the guest user.
71 + */
72 + public function getNormalUser()
73 + {
74 + return \BookStack\User::where('system_name', '=', null)->get()->last();
75 + }
76 +
77 + /**
70 * Quickly sets an array of settings. 78 * Quickly sets an array of settings.
71 * @param $settingsArray 79 * @param $settingsArray
72 */ 80 */
......
...@@ -77,4 +77,22 @@ class UserProfileTest extends TestCase ...@@ -77,4 +77,22 @@ class UserProfileTest extends TestCase
77 ->see($newUser->name); 77 ->see($newUser->name);
78 } 78 }
79 79
80 + public function test_guest_profile_shows_limited_form()
81 + {
82 + $this->asAdmin()
83 + ->visit('/settings/users')
84 + ->click('Guest')
85 + ->dontSeeElement('#password');
86 + }
87 +
88 + public function test_guest_profile_cannot_be_deleted()
89 + {
90 + $guestUser = \BookStack\User::getDefault();
91 + $this->asAdmin()->visit('/settings/users/' . $guestUser->id . '/delete')
92 + ->see('Delete User')->see('Guest')
93 + ->press('Confirm')
94 + ->seePageIs('/settings/users/' . $guestUser->id)
95 + ->see('cannot delete the guest user');
96 + }
97 +
80 } 98 }
......
1 +Hi, This is a test file for testing the upload process.
...\ No newline at end of file ...\ No newline at end of file
1 +v0.13-dev