]> BookStack Code Mirror - bookstack/blob - app/Search/SearchApiController.php
5072bd3b4631e745efaff7d3a5e4ff68715f0435
[bookstack] / app / Search / SearchApiController.php
1 <?php
2
3 namespace BookStack\Search;
4
5 use BookStack\Api\ApiEntityListFormatter;
6 use BookStack\Entities\Models\Entity;
7 use BookStack\Http\ApiController;
8 use Illuminate\Http\Request;
9
10 class SearchApiController extends ApiController
11 {
12     protected SearchRunner $searchRunner;
13     protected SearchResultsFormatter $resultsFormatter;
14
15     protected $rules = [
16         'all' => [
17             'query' => ['required'],
18             'page' => ['integer', 'min:1'],
19             'count' => ['integer', 'min:1', 'max:100'],
20             'include' => ['string', 'regex:/^[a-zA-Z,]*$/'],
21         ],
22     ];
23
24     /**
25      * Valid include parameters and their corresponding formatter methods.
26      * These parameters allow for additional related data, like titles or tags,
27      * to be included in the search results when requested via the API.
28      */
29     protected const VALID_INCLUDES = [
30         'titles' => 'withRelatedTitles',
31         'tags' => 'withTags',
32     ];
33
34     public function __construct(SearchRunner $searchRunner, SearchResultsFormatter $resultsFormatter)
35     {
36         $this->searchRunner = $searchRunner;
37         $this->resultsFormatter = $resultsFormatter;
38     }
39
40     /**
41      * Run a search query against all main content types (shelves, books, chapters & pages)
42      * in the system. Takes the same input as the main search bar within the BookStack
43      * interface as a 'query' parameter. See https://www.bookstackapp.com/docs/user/searching/
44      * for a full list of search term options. Results contain a 'type' property to distinguish
45      * between: bookshelf, book, chapter & page.
46      *
47      * This method now supports the 'include' parameter, which allows API clients to specify related
48      * fields (such as titles or tags) that should be included in the search results.
49      *
50      * The 'include' parameter is a comma-separated string. For example, adding `include=titles,tags`
51      * will include both titles and tags in the API response. If the parameter is not provided, only
52      * basic entity data will be returned.
53      *
54      * The paging parameters and response format emulates a standard listing endpoint
55      * but standard sorting and filtering cannot be done on this endpoint. If a count value
56      * is provided this will only be taken as a suggestion. The results in the response
57      * may currently be up to 4x this value.
58      */
59     public function all(Request $request)
60     {
61         $this->validate($request, $this->rules['all']);
62
63         $options = SearchOptions::fromString($request->get('query') ?? '');
64         $page = intval($request->get('page', '0')) ?: 1;
65         $count = min(intval($request->get('count', '0')) ?: 20, 100);
66         $includes = $this->parseIncludes($request->get('include', ''));
67
68         $results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
69         $this->resultsFormatter->format($results['results']->all(), $options);
70
71         $formatter = new ApiEntityListFormatter($results['results']->all());
72         $formatter->withType(); // Always include type as it's essential for search results
73
74         foreach ($includes as $include) {
75             if (isset(self::VALID_INCLUDES[$include])) {
76                 $method = self::VALID_INCLUDES[$include];
77                 $formatter->$method();
78             }
79         }
80
81         $formatter->withField('preview_html', function (Entity $entity) {
82             return [
83                 'name' => (string) $entity->getAttribute('preview_name'),
84                 'content' => (string) $entity->getAttribute('preview_content'),
85             ];
86         });
87
88         return response()->json([
89             'data' => $formatter->format(),
90             'total' => $results['total'],
91         ]);
92     }
93
94     /**
95      * Parse and validate the include parameter.
96      *
97      * @param string $includeString Comma-separated list of includes
98      * @return array<string>
99      */
100     protected function parseIncludes(string $includeString): array
101     {
102         if (empty($includeString)) {
103             return [];
104         }
105
106         return array_filter(
107             explode(',', strtolower($includeString)),
108             fn($include) => isset (self::VALID_INCLUDES[$include])
109         );
110     }
111 }