diff --git a/app/Entity.php b/app/Entity.php
index 496d20a33..2c447814f 100644
--- a/app/Entity.php
+++ b/app/Entity.php
@@ -160,44 +160,46 @@ class Entity extends Ownable
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
{
$exactTerms = [];
- if (count($terms) === 0) {
- $search = $this;
- $orderBy = 'updated_at';
- } else {
- foreach ($terms as $key => $term) {
- $term = htmlentities($term, ENT_QUOTES);
- $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
- if (preg_match('/".*?"/', $term)) {
- $term = str_replace('"', '', $term);
- $exactTerms[] = '%' . $term . '%';
- $term = '"' . $term . '"';
- } else {
- $term = '' . $term . '*';
- }
- if ($term !== '*') $terms[$key] = $term;
+ $fuzzyTerms = [];
+ $search = static::newQuery();
+ foreach ($terms as $key => $term) {
+ $safeTerm = htmlentities($term, ENT_QUOTES);
+ $safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm);
+ if (preg_match('/".*?"/', $safeTerm) || is_numeric($safeTerm)) {
+ $safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
+ $exactTerms[] = '%' . $safeTerm . '%';
+ } else {
+ $safeTerm = '' . $safeTerm . '*';
+ if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
}
- $termString = implode(' ', $terms);
- $fields = implode(',', $fieldsToSearch);
- $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
- $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
+ }
+ $isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
- // Ensure at least one exact term matches if in search
- if (count($exactTerms) > 0) {
- $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
- foreach ($exactTerms as $exactTerm) {
- foreach ($fieldsToSearch as $field) {
- $query->orWhere($field, 'like', $exactTerm);
- }
+ // Perform fulltext search if relevant terms exist.
+ if ($isFuzzy) {
+ $termString = implode(' ', $fuzzyTerms);
+ $fields = implode(',', $fieldsToSearch);
+ $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
+ $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
+ }
+
+ // Ensure at least one exact term matches if in search
+ if (count($exactTerms) > 0) {
+ $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
+ foreach ($exactTerms as $exactTerm) {
+ foreach ($fieldsToSearch as $field) {
+ $query->orWhere($field, 'like', $exactTerm);
}
- });
- }
- $orderBy = 'title_relevance';
- };
+ }
+ });
+ }
+ $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
+
// Load in relations
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php
index 038e444d2..4dc6583ea 100644
--- a/app/Http/Controllers/Auth/PasswordController.php
+++ b/app/Http/Controllers/Auth/PasswordController.php
@@ -4,6 +4,8 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;
+use Password;
class PasswordController extends Controller
{
@@ -29,4 +31,46 @@ class PasswordController extends Controller
{
$this->middleware('guest');
}
+
+
+ /**
+ * Send a reset link to the given user.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\Response
+ */
+ public function sendResetLinkEmail(Request $request)
+ {
+ $this->validate($request, ['email' => 'required|email']);
+
+ $broker = $this->getBroker();
+
+ $response = Password::broker($broker)->sendResetLink(
+ $request->only('email'), $this->resetEmailBuilder()
+ );
+
+ switch ($response) {
+ case Password::RESET_LINK_SENT:
+ $message = 'A password reset link has been sent to ' . $request->get('email') . '.';
+ session()->flash('success', $message);
+ return $this->getSendResetLinkEmailSuccessResponse($response);
+
+ case Password::INVALID_USER:
+ default:
+ return $this->getSendResetLinkEmailFailureResponse($response);
+ }
+ }
+
+ /**
+ * Get the response for after a successful password reset.
+ *
+ * @param string $response
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ protected function getResetSuccessResponse($response)
+ {
+ $message = 'Your password has been successfully reset.';
+ session()->flash('success', $message);
+ return redirect($this->redirectPath())->with('status', trans($response));
+ }
}
diff --git a/app/helpers.php b/app/helpers.php
index b8abb1006..c12708877 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -84,6 +84,11 @@ function baseUrl($path, $forceAppDomain = false)
$path = implode('/', array_splice($explodedPath, 3));
}
+ // Return normal url path if not specified in config
+ if (config('app.url') === '') {
+ return url($path);
+ }
+
return rtrim(config('app.url'), '/') . '/' . $path;
}
diff --git a/config/setting-defaults.php b/config/setting-defaults.php
index 24a49c364..deafceb29 100644
--- a/config/setting-defaults.php
+++ b/config/setting-defaults.php
@@ -8,6 +8,8 @@ return [
'app-name' => 'BookStack',
'app-editor' => 'wysiwyg',
'app-color' => '#0288D1',
- 'app-color-light' => 'rgba(21, 101, 192, 0.15)'
+ 'app-color-light' => 'rgba(21, 101, 192, 0.15)',
+ 'app-custom-head' => false,
+ 'registration-enabled' => false,
];
\ No newline at end of file
diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss
index 3c7f7490b..727633f75 100644
--- a/resources/assets/sass/_blocks.scss
+++ b/resources/assets/sass/_blocks.scss
@@ -135,6 +135,7 @@
border-left: 3px solid #BBB;
background-color: #EEE;
padding: $-s;
+ display: flex;
&:before {
font-family: 'Material-Design-Iconic-Font';
padding-right: $-s;
diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss
index cd81bb4e2..8bf09a626 100644
--- a/resources/assets/sass/_text.scss
+++ b/resources/assets/sass/_text.scss
@@ -252,7 +252,7 @@ ul {
ol {
list-style: decimal;
- padding-left: $-m * 1.3;
+ padding-left: $-m * 2;
overflow: hidden;
}
diff --git a/resources/views/auth/password.blade.php b/resources/views/auth/password.blade.php
index d8536efa7..115785ab2 100644
--- a/resources/views/auth/password.blade.php
+++ b/resources/views/auth/password.blade.php
@@ -1,5 +1,12 @@
@extends('public')
+@section('header-buttons')
+ Sign in
+ @if(setting('registration-enabled'))
+ Sign up
+ @endif
+@stop
+
@section('content')
diff --git a/resources/views/auth/reset.blade.php b/resources/views/auth/reset.blade.php
index 9a9a65ff0..612b50ff8 100644
--- a/resources/views/auth/reset.blade.php
+++ b/resources/views/auth/reset.blade.php
@@ -1,5 +1,12 @@
@extends('public')
+@section('header-buttons')
+ Sign in
+ @if(setting('registration-enabled'))
+ Sign up
+ @endif
+@stop
+
@section('body-class', 'image-cover login')
@section('content')
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index be47abdca..961ead251 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -23,7 +23,7 @@
@include('partials/custom-styles')
- @if(setting('app-custom-head', false))
+ @if(setting('app-custom-head'))
{!! setting('app-custom-head') !!}
@endif
diff --git a/resources/views/emails/email-confirmation.blade.php b/resources/views/emails/email-confirmation.blade.php
index 6a0dc0378..0a211c369 100644
--- a/resources/views/emails/email-confirmation.blade.php
+++ b/resources/views/emails/email-confirmation.blade.php
@@ -162,14 +162,14 @@
Email Confirmation
- Thanks for joining {{ setting('app-name')}}.
+ Thanks for joining {{ setting('app-name')}}.
Please confirm your email address by clicking the button below.
|
- Confirm
Email
|
diff --git a/resources/views/emails/password.blade.php b/resources/views/emails/password.blade.php
index dfd8f3db5..a8f7911e0 100644
--- a/resources/views/emails/password.blade.php
+++ b/resources/views/emails/password.blade.php
@@ -1 +1 @@
-
Password Reset From {{ setting('app-name')}}
|
Password Reset
A password reset was requested for this email address on {{ setting('app-name')}}. If you did not request
a password change please ignore this email.
|
|
|
\ No newline at end of file
+
Password Reset From {{ setting('app-name')}}
|
Password Reset
A password reset was requested for this email address on {{ setting('app-name')}}. If you did not request
a password change please ignore this email.
|
|
|
\ No newline at end of file
diff --git a/resources/views/pages/pdf.blade.php b/resources/views/pages/pdf.blade.php
index 0cbf4df02..5c9fd5eea 100644
--- a/resources/views/pages/pdf.blade.php
+++ b/resources/views/pages/pdf.blade.php
@@ -14,7 +14,7 @@
table {
max-width: 800px !important;
font-size: 0.8em;
- width: auto !important;
+ width: 100% !important;
}
table td {
diff --git a/resources/views/public.blade.php b/resources/views/public.blade.php
index 2de4d968a..2bce41570 100644
--- a/resources/views/public.blade.php
+++ b/resources/views/public.blade.php
@@ -17,6 +17,11 @@
@include('partials/custom-styles')
+
+
+ @if(setting('app-custom-head'))
+ {!! setting('app-custom-head') !!}
+ @endif
diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php
index 306771ed5..99885d552 100644
--- a/tests/Auth/AuthTest.php
+++ b/tests/Auth/AuthTest.php
@@ -216,6 +216,37 @@ class AuthTest extends TestCase
->seePageIs('/login');
}
+ public function test_reset_password_flow()
+ {
+ $this->visit('/login')->click('Forgot Password?')
+ ->seePageIs('/password/email')
+ ->type('admin@admin.com', 'email')
+ ->press('Send Reset Link')
+ ->see('A password reset link has been sent to admin@admin.com');
+
+ $this->seeInDatabase('password_resets', [
+ 'email' => 'admin@admin.com'
+ ]);
+
+ $reset = DB::table('password_resets')->where('email', '=', 'admin@admin.com')->first();
+ $this->visit('/password/reset/' . $reset->token)
+ ->see('Reset Password')
+ ->submitForm('Reset Password', [
+ 'email' => 'admin@admin.com',
+ 'password' => 'randompass',
+ 'password_confirmation' => 'randompass'
+ ])->seePageIs('/')
+ ->see('Your password has been successfully reset');
+ }
+
+ public function test_reset_password_page_shows_sign_links()
+ {
+ $this->setSettings(['registration-enabled' => 'true']);
+ $this->visit('/password/email')
+ ->seeLink('Sign in')
+ ->seeLink('Sign up');
+ }
+
/**
* Perform a login
* @param string $email
diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php
index 8adfd35a3..cfdabdb0a 100644
--- a/tests/Entity/EntitySearchTest.php
+++ b/tests/Entity/EntitySearchTest.php
@@ -91,6 +91,12 @@ class EntitySearchTest extends TestCase
->see('Book Search Results')->see('.entity-list', $book->name);
}
+ public function test_searching_hypen_doesnt_break()
+ {
+ $this->visit('/search/all?term=cat+-')
+ ->seeStatusCode(200);
+ }
+
public function test_ajax_entity_search()
{
$page = \BookStack\Page::all()->last();
diff --git a/tests/ImageTest.php b/tests/ImageTest.php
index 806a36acc..23373419f 100644
--- a/tests/ImageTest.php
+++ b/tests/ImageTest.php
@@ -57,10 +57,12 @@ class ImageTest extends TestCase
$relPath = $this->uploadImage($imageName, $page->id);
$this->assertResponseOk();
- $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists');
+ $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath));
+
+ $this->deleteImage($relPath);
$this->seeInDatabase('images', [
- 'url' => $relPath,
+ 'url' => url($relPath),
'type' => 'gallery',
'uploaded_to' => $page->id,
'path' => $relPath,
@@ -68,8 +70,7 @@ class ImageTest extends TestCase
'updated_by' => $admin->id,
'name' => $imageName
]);
-
- $this->deleteImage($relPath);
+
}
public function test_image_delete()