Showing
6 changed files
with
117 additions
and
12 deletions
| ... | @@ -157,9 +157,13 @@ class Entity extends Ownable | ... | @@ -157,9 +157,13 @@ class Entity extends Ownable |
| 157 | * @param string[] array $wheres | 157 | * @param string[] array $wheres |
| 158 | * @return mixed | 158 | * @return mixed |
| 159 | */ | 159 | */ |
| 160 | - public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) | 160 | + public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) |
| 161 | { | 161 | { |
| 162 | $exactTerms = []; | 162 | $exactTerms = []; |
| 163 | + if (count($terms) === 0) { | ||
| 164 | + $search = $this; | ||
| 165 | + $orderBy = 'updated_at'; | ||
| 166 | + } else { | ||
| 163 | foreach ($terms as $key => $term) { | 167 | foreach ($terms as $key => $term) { |
| 164 | $term = htmlentities($term, ENT_QUOTES); | 168 | $term = htmlentities($term, ENT_QUOTES); |
| 165 | $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); | 169 | $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); |
| ... | @@ -186,19 +190,21 @@ class Entity extends Ownable | ... | @@ -186,19 +190,21 @@ class Entity extends Ownable |
| 186 | } | 190 | } |
| 187 | }); | 191 | }); |
| 188 | } | 192 | } |
| 193 | + $orderBy = 'title_relevance'; | ||
| 194 | + }; | ||
| 189 | 195 | ||
| 190 | // Add additional where terms | 196 | // Add additional where terms |
| 191 | foreach ($wheres as $whereTerm) { | 197 | foreach ($wheres as $whereTerm) { |
| 192 | $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); | 198 | $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); |
| 193 | } | 199 | } |
| 194 | // Load in relations | 200 | // Load in relations |
| 195 | - if (static::isA('page')) { | 201 | + if ($this->isA('page')) { |
| 196 | $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); | 202 | $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); |
| 197 | - } else if (static::isA('chapter')) { | 203 | + } else if ($this->isA('chapter')) { |
| 198 | $search = $search->with('book'); | 204 | $search = $search->with('book'); |
| 199 | } | 205 | } |
| 200 | 206 | ||
| 201 | - return $search->orderBy('title_relevance', 'desc'); | 207 | + return $search->orderBy($orderBy, 'desc'); |
| 202 | } | 208 | } |
| 203 | 209 | ||
| 204 | } | 210 | } | ... | ... |
| ... | @@ -286,8 +286,9 @@ class BookRepo extends EntityRepo | ... | @@ -286,8 +286,9 @@ class BookRepo extends EntityRepo |
| 286 | public function getBySearch($term, $count = 20, $paginationAppends = []) | 286 | public function getBySearch($term, $count = 20, $paginationAppends = []) |
| 287 | { | 287 | { |
| 288 | $terms = $this->prepareSearchTerms($term); | 288 | $terms = $this->prepareSearchTerms($term); |
| 289 | - $books = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) | 289 | + $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)); |
| 290 | - ->paginate($count)->appends($paginationAppends); | 290 | + $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term); |
| 291 | + $books = $bookQuery->paginate($count)->appends($paginationAppends); | ||
| 291 | $words = join('|', explode(' ', preg_quote(trim($term), '/'))); | 292 | $words = join('|', explode(' ', preg_quote(trim($term), '/'))); |
| 292 | foreach ($books as $book) { | 293 | foreach ($books as $book) { |
| 293 | //highlight | 294 | //highlight | ... | ... |
| ... | @@ -168,8 +168,9 @@ class ChapterRepo extends EntityRepo | ... | @@ -168,8 +168,9 @@ class ChapterRepo extends EntityRepo |
| 168 | public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) | 168 | public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) |
| 169 | { | 169 | { |
| 170 | $terms = $this->prepareSearchTerms($term); | 170 | $terms = $this->prepareSearchTerms($term); |
| 171 | - $chapters = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) | 171 | + $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)); |
| 172 | - ->paginate($count)->appends($paginationAppends); | 172 | + $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term); |
| 173 | + $chapters = $chapterQuery->paginate($count)->appends($paginationAppends); | ||
| 173 | $words = join('|', explode(' ', preg_quote(trim($term), '/'))); | 174 | $words = join('|', explode(' ', preg_quote(trim($term), '/'))); |
| 174 | foreach ($chapters as $chapter) { | 175 | foreach ($chapters as $chapter) { |
| 175 | //highlight | 176 | //highlight | ... | ... |
| ... | @@ -6,6 +6,7 @@ use BookStack\Entity; | ... | @@ -6,6 +6,7 @@ use BookStack\Entity; |
| 6 | use BookStack\Page; | 6 | use BookStack\Page; |
| 7 | use BookStack\Services\PermissionService; | 7 | use BookStack\Services\PermissionService; |
| 8 | use BookStack\User; | 8 | use BookStack\User; |
| 9 | +use Illuminate\Support\Facades\Log; | ||
| 9 | 10 | ||
| 10 | class EntityRepo | 11 | class EntityRepo |
| 11 | { | 12 | { |
| ... | @@ -31,6 +32,12 @@ class EntityRepo | ... | @@ -31,6 +32,12 @@ class EntityRepo |
| 31 | protected $permissionService; | 32 | protected $permissionService; |
| 32 | 33 | ||
| 33 | /** | 34 | /** |
| 35 | + * Acceptable operators to be used in a query | ||
| 36 | + * @var array | ||
| 37 | + */ | ||
| 38 | + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; | ||
| 39 | + | ||
| 40 | + /** | ||
| 34 | * EntityService constructor. | 41 | * EntityService constructor. |
| 35 | */ | 42 | */ |
| 36 | public function __construct() | 43 | public function __construct() |
| ... | @@ -163,6 +170,7 @@ class EntityRepo | ... | @@ -163,6 +170,7 @@ class EntityRepo |
| 163 | */ | 170 | */ |
| 164 | protected function prepareSearchTerms($termString) | 171 | protected function prepareSearchTerms($termString) |
| 165 | { | 172 | { |
| 173 | + $termString = $this->cleanSearchTermString($termString); | ||
| 166 | preg_match_all('/"(.*?)"/', $termString, $matches); | 174 | preg_match_all('/"(.*?)"/', $termString, $matches); |
| 167 | if (count($matches[1]) > 0) { | 175 | if (count($matches[1]) > 0) { |
| 168 | $terms = $matches[1]; | 176 | $terms = $matches[1]; |
| ... | @@ -174,5 +182,93 @@ class EntityRepo | ... | @@ -174,5 +182,93 @@ class EntityRepo |
| 174 | return $terms; | 182 | return $terms; |
| 175 | } | 183 | } |
| 176 | 184 | ||
| 185 | + /** | ||
| 186 | + * Removes any special search notation that should not | ||
| 187 | + * be used in a full-text search. | ||
| 188 | + * @param $termString | ||
| 189 | + * @return mixed | ||
| 190 | + */ | ||
| 191 | + protected function cleanSearchTermString($termString) | ||
| 192 | + { | ||
| 193 | + // Strip tag searches | ||
| 194 | + $termString = preg_replace('/\[.*?\]/', '', $termString); | ||
| 195 | + // Reduced multiple spacing into single spacing | ||
| 196 | + $termString = preg_replace("/\s{2,}/", " ", $termString); | ||
| 197 | + return $termString; | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + /** | ||
| 201 | + * Get the available query operators as a regex escaped list. | ||
| 202 | + * @return mixed | ||
| 203 | + */ | ||
| 204 | + protected function getRegexEscapedOperators() | ||
| 205 | + { | ||
| 206 | + $escapedOperators = []; | ||
| 207 | + foreach ($this->queryOperators as $operator) { | ||
| 208 | + $escapedOperators[] = preg_quote($operator); | ||
| 209 | + } | ||
| 210 | + return join('|', $escapedOperators); | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + /** | ||
| 214 | + * Parses advanced search notations and adds them to the db query. | ||
| 215 | + * @param $query | ||
| 216 | + * @param $termString | ||
| 217 | + * @return mixed | ||
| 218 | + */ | ||
| 219 | + protected function addAdvancedSearchQueries($query, $termString) | ||
| 220 | + { | ||
| 221 | + $escapedOperators = $this->getRegexEscapedOperators(); | ||
| 222 | + // Look for tag searches | ||
| 223 | + preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags); | ||
| 224 | + if (count($tags[0]) > 0) { | ||
| 225 | + $this->applyTagSearches($query, $tags); | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + return $query; | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + /** | ||
| 232 | + * Apply extracted tag search terms onto a entity query. | ||
| 233 | + * @param $query | ||
| 234 | + * @param $tags | ||
| 235 | + * @return mixed | ||
| 236 | + */ | ||
| 237 | + protected function applyTagSearches($query, $tags) { | ||
| 238 | + $query->where(function($query) use ($tags) { | ||
| 239 | + foreach ($tags[1] as $index => $tagName) { | ||
| 240 | + $query->whereHas('tags', function($query) use ($tags, $index, $tagName) { | ||
| 241 | + $tagOperator = $tags[3][$index]; | ||
| 242 | + $tagValue = $tags[4][$index]; | ||
| 243 | + if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) { | ||
| 244 | + if (is_numeric($tagValue) && $tagOperator !== 'like') { | ||
| 245 | + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will | ||
| 246 | + // search the value as a string which prevents being able to do number-based operations | ||
| 247 | + // on the tag values. We ensure it has a numeric value and then cast it just to be sure. | ||
| 248 | + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'"); | ||
| 249 | + $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}"); | ||
| 250 | + } else { | ||
| 251 | + $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue); | ||
| 252 | + } | ||
| 253 | + } else { | ||
| 254 | + $query->where('name', '=', $tagName); | ||
| 255 | + } | ||
| 256 | + }); | ||
| 257 | + } | ||
| 258 | + }); | ||
| 259 | + return $query; | ||
| 260 | + } | ||
| 177 | 261 | ||
| 178 | } | 262 | } |
| 263 | + | ||
| 264 | + | ||
| 265 | + | ||
| 266 | + | ||
| 267 | + | ||
| 268 | + | ||
| 269 | + | ||
| 270 | + | ||
| 271 | + | ||
| 272 | + | ||
| 273 | + | ||
| 274 | + | ... | ... |
| ... | @@ -245,8 +245,9 @@ class PageRepo extends EntityRepo | ... | @@ -245,8 +245,9 @@ class PageRepo extends EntityRepo |
| 245 | public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) | 245 | public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) |
| 246 | { | 246 | { |
| 247 | $terms = $this->prepareSearchTerms($term); | 247 | $terms = $this->prepareSearchTerms($term); |
| 248 | - $pages = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) | 248 | + $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)); |
| 249 | - ->paginate($count)->appends($paginationAppends); | 249 | + $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term); |
| 250 | + $pages = $pageQuery->paginate($count)->appends($paginationAppends); | ||
| 250 | 251 | ||
| 251 | // Add highlights to page text. | 252 | // Add highlights to page text. |
| 252 | $words = join('|', explode(' ', preg_quote(trim($term), '/'))); | 253 | $words = join('|', explode(' ', preg_quote(trim($term), '/'))); | ... | ... |
| ... | @@ -8,8 +8,8 @@ | ... | @@ -8,8 +8,8 @@ |
| 8 | <table> | 8 | <table> |
| 9 | @foreach($page->tags as $tag) | 9 | @foreach($page->tags as $tag) |
| 10 | <tr class="tag"> | 10 | <tr class="tag"> |
| 11 | - <td @if(!$tag->value) colspan="2" @endif> {{ $tag->name }}</td> | 11 | + <td @if(!$tag->value) colspan="2" @endif><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%5D">{{ $tag->name }}</a></td> |
| 12 | - @if($tag->value) <td class="tag-value">{{$tag->value}}</td> @endif | 12 | + @if($tag->value) <td class="tag-value"><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%3D{{ urlencode($tag->value) }}%5D">{{$tag->value}}</a></td> @endif |
| 13 | </tr> | 13 | </tr> |
| 14 | @endforeach | 14 | @endforeach |
| 15 | </table> | 15 | </table> | ... | ... |
-
Please register or sign in to post a comment