Dan Brown

Merge branch 'v0.12' into release

...@@ -160,44 +160,46 @@ class Entity extends Ownable ...@@ -160,44 +160,46 @@ class Entity extends Ownable
160 public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) 160 public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
161 { 161 {
162 $exactTerms = []; 162 $exactTerms = [];
163 - if (count($terms) === 0) { 163 + $fuzzyTerms = [];
164 - $search = $this; 164 + $search = static::newQuery();
165 - $orderBy = 'updated_at'; 165 + foreach ($terms as $key => $term) {
166 - } else { 166 + $safeTerm = htmlentities($term, ENT_QUOTES);
167 - foreach ($terms as $key => $term) { 167 + $safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm);
168 - $term = htmlentities($term, ENT_QUOTES); 168 + if (preg_match('/&quot;.*?&quot;/', $safeTerm) || is_numeric($safeTerm)) {
169 - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); 169 + $safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
170 - if (preg_match('/&quot;.*?&quot;/', $term)) { 170 + $exactTerms[] = '%' . $safeTerm . '%';
171 - $term = str_replace('&quot;', '', $term); 171 + } else {
172 - $exactTerms[] = '%' . $term . '%'; 172 + $safeTerm = '' . $safeTerm . '*';
173 - $term = '"' . $term . '"'; 173 + if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
174 - } else {
175 - $term = '' . $term . '*';
176 - }
177 - if ($term !== '*') $terms[$key] = $term;
178 } 174 }
179 - $termString = implode(' ', $terms); 175 + }
176 + $isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
177 +
178 + // Perform fulltext search if relevant terms exist.
179 + if ($isFuzzy) {
180 + $termString = implode(' ', $fuzzyTerms);
180 $fields = implode(',', $fieldsToSearch); 181 $fields = implode(',', $fieldsToSearch);
181 - $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); 182 + $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
182 $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); 183 $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
184 + }
183 185
184 - // Ensure at least one exact term matches if in search 186 + // Ensure at least one exact term matches if in search
185 - if (count($exactTerms) > 0) { 187 + if (count($exactTerms) > 0) {
186 - $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { 188 + $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
187 - foreach ($exactTerms as $exactTerm) { 189 + foreach ($exactTerms as $exactTerm) {
188 - foreach ($fieldsToSearch as $field) { 190 + foreach ($fieldsToSearch as $field) {
189 - $query->orWhere($field, 'like', $exactTerm); 191 + $query->orWhere($field, 'like', $exactTerm);
190 - }
191 } 192 }
192 - }); 193 + }
193 - } 194 + });
194 - $orderBy = 'title_relevance'; 195 + }
195 - }; 196 + $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
196 197
197 // Add additional where terms 198 // Add additional where terms
198 foreach ($wheres as $whereTerm) { 199 foreach ($wheres as $whereTerm) {
199 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); 200 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
200 } 201 }
202 +
201 // Load in relations 203 // Load in relations
202 if ($this->isA('page')) { 204 if ($this->isA('page')) {
203 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); 205 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
......
...@@ -4,6 +4,8 @@ namespace BookStack\Http\Controllers\Auth; ...@@ -4,6 +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;
7 9
8 class PasswordController extends Controller 10 class PasswordController extends Controller
9 { 11 {
...@@ -29,4 +31,46 @@ class PasswordController extends Controller ...@@ -29,4 +31,46 @@ class PasswordController extends Controller
29 { 31 {
30 $this->middleware('guest'); 32 $this->middleware('guest');
31 } 33 }
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 + }
63 +
64 + /**
65 + * Get the response for after a successful password reset.
66 + *
67 + * @param string $response
68 + * @return \Symfony\Component\HttpFoundation\Response
69 + */
70 + protected function getResetSuccessResponse($response)
71 + {
72 + $message = 'Your password has been successfully reset.';
73 + session()->flash('success', $message);
74 + return redirect($this->redirectPath())->with('status', trans($response));
75 + }
32 } 76 }
......
...@@ -84,6 +84,11 @@ function baseUrl($path, $forceAppDomain = false) ...@@ -84,6 +84,11 @@ function baseUrl($path, $forceAppDomain = false)
84 $path = implode('/', array_splice($explodedPath, 3)); 84 $path = implode('/', array_splice($explodedPath, 3));
85 } 85 }
86 86
87 + // Return normal url path if not specified in config
88 + if (config('app.url') === '') {
89 + return url($path);
90 + }
91 +
87 return rtrim(config('app.url'), '/') . '/' . $path; 92 return rtrim(config('app.url'), '/') . '/' . $path;
88 } 93 }
89 94
......
...@@ -8,6 +8,8 @@ return [ ...@@ -8,6 +8,8 @@ return [
8 'app-name' => 'BookStack', 8 'app-name' => 'BookStack',
9 'app-editor' => 'wysiwyg', 9 'app-editor' => 'wysiwyg',
10 'app-color' => '#0288D1', 10 'app-color' => '#0288D1',
11 - 'app-color-light' => 'rgba(21, 101, 192, 0.15)' 11 + 'app-color-light' => 'rgba(21, 101, 192, 0.15)',
12 + 'app-custom-head' => false,
13 + 'registration-enabled' => false,
12 14
13 ]; 15 ];
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -135,6 +135,7 @@ ...@@ -135,6 +135,7 @@
135 border-left: 3px solid #BBB; 135 border-left: 3px solid #BBB;
136 background-color: #EEE; 136 background-color: #EEE;
137 padding: $-s; 137 padding: $-s;
138 + display: flex;
138 &:before { 139 &:before {
139 font-family: 'Material-Design-Iconic-Font'; 140 font-family: 'Material-Design-Iconic-Font';
140 padding-right: $-s; 141 padding-right: $-s;
......
...@@ -252,7 +252,7 @@ ul { ...@@ -252,7 +252,7 @@ ul {
252 252
253 ol { 253 ol {
254 list-style: decimal; 254 list-style: decimal;
255 - padding-left: $-m * 1.3; 255 + padding-left: $-m * 2;
256 overflow: hidden; 256 overflow: hidden;
257 } 257 }
258 258
......
1 @extends('public') 1 @extends('public')
2 2
3 +@section('header-buttons')
4 + <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
5 + @if(setting('registration-enabled'))
6 + <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
7 + @endif
8 +@stop
9 +
3 @section('content') 10 @section('content')
4 11
5 12
......
1 @extends('public') 1 @extends('public')
2 2
3 +@section('header-buttons')
4 + <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
5 + @if(setting('registration-enabled'))
6 + <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
7 + @endif
8 +@stop
9 +
3 @section('body-class', 'image-cover login') 10 @section('body-class', 'image-cover login')
4 11
5 @section('content') 12 @section('content')
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
23 @include('partials/custom-styles') 23 @include('partials/custom-styles')
24 24
25 <!-- Custom user content --> 25 <!-- Custom user content -->
26 - @if(setting('app-custom-head', false)) 26 + @if(setting('app-custom-head'))
27 {!! setting('app-custom-head') !!} 27 {!! setting('app-custom-head') !!}
28 @endif 28 @endif
29 </head> 29 </head>
......
...@@ -162,14 +162,14 @@ ...@@ -162,14 +162,14 @@
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;"> 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> 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;"> 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('/') }}">{{ setting('app-name')}}</a>. <br/> 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> 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%;"> 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;"> 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" 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;"> 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;"> 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) }}" 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 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> 174 Email</a></p>
175 </td> 175 </td>
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
14 table { 14 table {
15 max-width: 800px !important; 15 max-width: 800px !important;
16 font-size: 0.8em; 16 font-size: 0.8em;
17 - width: auto !important; 17 + width: 100% !important;
18 } 18 }
19 19
20 table td { 20 table td {
......
...@@ -17,6 +17,11 @@ ...@@ -17,6 +17,11 @@
17 <!-- Scripts --> 17 <!-- Scripts -->
18 <script src="{{ baseUrl("/libs/jquery/jquery.min.js?version=2.1.4") }}"></script> 18 <script src="{{ baseUrl("/libs/jquery/jquery.min.js?version=2.1.4") }}"></script>
19 @include('partials/custom-styles') 19 @include('partials/custom-styles')
20 +
21 + <!-- Custom user content -->
22 + @if(setting('app-custom-head'))
23 + {!! setting('app-custom-head') !!}
24 + @endif
20 </head> 25 </head>
21 <body class="@yield('body-class')" ng-app="bookStack"> 26 <body class="@yield('body-class')" ng-app="bookStack">
22 27
......
...@@ -216,6 +216,37 @@ class AuthTest extends TestCase ...@@ -216,6 +216,37 @@ class AuthTest extends TestCase
216 ->seePageIs('/login'); 216 ->seePageIs('/login');
217 } 217 }
218 218
219 + public function test_reset_password_flow()
220 + {
221 + $this->visit('/login')->click('Forgot Password?')
222 + ->seePageIs('/password/email')
223 + ->type('admin@admin.com', 'email')
224 + ->press('Send Reset Link')
225 + ->see('A password reset link has been sent to admin@admin.com');
226 +
227 + $this->seeInDatabase('password_resets', [
228 + 'email' => 'admin@admin.com'
229 + ]);
230 +
231 + $reset = DB::table('password_resets')->where('email', '=', 'admin@admin.com')->first();
232 + $this->visit('/password/reset/' . $reset->token)
233 + ->see('Reset Password')
234 + ->submitForm('Reset Password', [
235 + 'email' => 'admin@admin.com',
236 + 'password' => 'randompass',
237 + 'password_confirmation' => 'randompass'
238 + ])->seePageIs('/')
239 + ->see('Your password has been successfully reset');
240 + }
241 +
242 + public function test_reset_password_page_shows_sign_links()
243 + {
244 + $this->setSettings(['registration-enabled' => 'true']);
245 + $this->visit('/password/email')
246 + ->seeLink('Sign in')
247 + ->seeLink('Sign up');
248 + }
249 +
219 /** 250 /**
220 * Perform a login 251 * Perform a login
221 * @param string $email 252 * @param string $email
......
...@@ -91,6 +91,12 @@ class EntitySearchTest extends TestCase ...@@ -91,6 +91,12 @@ class EntitySearchTest extends TestCase
91 ->see('Book Search Results')->see('.entity-list', $book->name); 91 ->see('Book Search Results')->see('.entity-list', $book->name);
92 } 92 }
93 93
94 + public function test_searching_hypen_doesnt_break()
95 + {
96 + $this->visit('/search/all?term=cat+-')
97 + ->seeStatusCode(200);
98 + }
99 +
94 public function test_ajax_entity_search() 100 public function test_ajax_entity_search()
95 { 101 {
96 $page = \BookStack\Page::all()->last(); 102 $page = \BookStack\Page::all()->last();
......
...@@ -57,10 +57,12 @@ class ImageTest extends TestCase ...@@ -57,10 +57,12 @@ class ImageTest extends TestCase
57 $relPath = $this->uploadImage($imageName, $page->id); 57 $relPath = $this->uploadImage($imageName, $page->id);
58 $this->assertResponseOk(); 58 $this->assertResponseOk();
59 59
60 - $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists'); 60 + $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath));
61 +
62 + $this->deleteImage($relPath);
61 63
62 $this->seeInDatabase('images', [ 64 $this->seeInDatabase('images', [
63 - 'url' => $relPath, 65 + 'url' => url($relPath),
64 'type' => 'gallery', 66 'type' => 'gallery',
65 'uploaded_to' => $page->id, 67 'uploaded_to' => $page->id,
66 'path' => $relPath, 68 'path' => $relPath,
...@@ -68,8 +70,7 @@ class ImageTest extends TestCase ...@@ -68,8 +70,7 @@ class ImageTest extends TestCase
68 'updated_by' => $admin->id, 70 'updated_by' => $admin->id,
69 'name' => $imageName 71 'name' => $imageName
70 ]); 72 ]);
71 - 73 +
72 - $this->deleteImage($relPath);
73 } 74 }
74 75
75 public function test_image_delete() 76 public function test_image_delete()
......