Merge "jqueryMsg: Allow sneaking in non-qqx JavaScript messages in qqx mode"
[lhc/web/wiklou.git] / includes / search / SearchResultSet.php
1 <?php
2 /**
3 * Search result sets
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Search
22 */
23
24 /**
25 * @ingroup Search
26 */
27 class SearchResultSet implements Countable, IteratorAggregate {
28
29 /**
30 * Identifier for interwiki results that are displayed only together with existing main wiki
31 * results.
32 */
33 const SECONDARY_RESULTS = 0;
34
35 /**
36 * Identifier for interwiki results that can be displayed even if no existing main wiki results
37 * exist.
38 */
39 const INLINE_RESULTS = 1;
40
41 protected $containedSyntax = false;
42
43 /**
44 * Cache of titles.
45 * Lists titles of the result set, in the same order as results.
46 * @var Title[]
47 */
48 private $titles;
49
50 /**
51 * Cache of results - serialization of the result iterator
52 * as an array.
53 * @var SearchResult[]
54 */
55 protected $results;
56
57 /**
58 * Set of result's extra data, indexed per result id
59 * and then per data item name.
60 * The structure is:
61 * PAGE_ID => [ augmentor name => data, ... ]
62 * @var array[]
63 */
64 protected $extraData = [];
65
66 /**
67 * @var boolean True when there are more pages of search results available.
68 */
69 private $hasMoreResults;
70
71 /**
72 * @var ArrayIterator|null Iterator supporting BC iteration methods
73 */
74 private $bcIterator;
75
76 /**
77 * @param bool $containedSyntax True when query is not requesting a simple
78 * term match
79 * @param bool $hasMoreResults True when there are more pages of search
80 * results available.
81 */
82 public function __construct( $containedSyntax = false, $hasMoreResults = false ) {
83 if ( static::class === self::class ) {
84 // This class will eventually be abstract. SearchEngine implementations
85 // already have to extend this class anyways to provide the actual
86 // search results.
87 wfDeprecated( __METHOD__, '1.32' );
88 }
89 $this->containedSyntax = $containedSyntax;
90 $this->hasMoreResults = $hasMoreResults;
91 }
92
93 /**
94 * Fetch an array of regular expression fragments for matching
95 * the search terms as parsed by this engine in a text extract.
96 * STUB
97 *
98 * @return array
99 */
100 function termMatches() {
101 return [];
102 }
103
104 function numRows() {
105 return $this->count();
106 }
107
108 final public function count() {
109 return count( $this->extractResults() );
110 }
111
112 /**
113 * Some search modes return a total hit count for the query
114 * in the entire article database. This may include pages
115 * in namespaces that would not be matched on the given
116 * settings.
117 *
118 * Return null if no total hits number is supported.
119 *
120 * @return int
121 */
122 function getTotalHits() {
123 return null;
124 }
125
126 /**
127 * Some search modes will run an alternative query that it thinks gives
128 * a better result than the provided search. Returns true if this has
129 * occurred.
130 *
131 * @return bool
132 */
133 function hasRewrittenQuery() {
134 return false;
135 }
136
137 /**
138 * @return string|null The search the query was internally rewritten to,
139 * or null when the result of the original query was returned.
140 */
141 function getQueryAfterRewrite() {
142 return null;
143 }
144
145 /**
146 * @return string|null Same as self::getQueryAfterRewrite(), but in HTML
147 * and with changes highlighted. Null when the query was not rewritten.
148 */
149 function getQueryAfterRewriteSnippet() {
150 return null;
151 }
152
153 /**
154 * Some search modes return a suggested alternate term if there are
155 * no exact hits. Returns true if there is one on this set.
156 *
157 * @return bool
158 */
159 function hasSuggestion() {
160 return false;
161 }
162
163 /**
164 * @return string|null Suggested query, null if none
165 */
166 function getSuggestionQuery() {
167 return null;
168 }
169
170 /**
171 * @return string HTML highlighted suggested query, '' if none
172 */
173 function getSuggestionSnippet() {
174 return '';
175 }
176
177 /**
178 * Return a result set of hits on other (multiple) wikis associated with this one
179 *
180 * @param int $type
181 * @return SearchResultSet[]
182 */
183 function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
184 return null;
185 }
186
187 /**
188 * Check if there are results on other wikis
189 *
190 * @param int $type
191 * @return bool
192 */
193 function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
194 return false;
195 }
196
197 /**
198 * Fetches next search result, or false.
199 * @deprecated since 1.32; Use self::extractResults() or foreach
200 * @return SearchResult|false
201 */
202 public function next() {
203 wfDeprecated( __METHOD__, '1.32' );
204 $it = $this->bcIterator();
205 $searchResult = $it->current();
206 $it->next();
207 return $searchResult ?? false;
208 }
209
210 /**
211 * Rewind result set back to beginning
212 * @deprecated since 1.32; Use self::extractResults() or foreach
213 */
214 public function rewind() {
215 wfDeprecated( __METHOD__, '1.32' );
216 $this->bcIterator()->rewind();
217 }
218
219 private function bcIterator() {
220 if ( $this->bcIterator === null ) {
221 $this->bcIterator = 'RECURSION';
222 $this->bcIterator = $this->getIterator();
223 } elseif ( $this->bcIterator === 'RECURSION' ) {
224 // Either next/rewind or extractResults must be implemented. This
225 // class was potentially instantiated directly. It should be
226 // abstract with abstract methods to enforce this but that's a
227 // breaking change...
228 wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
229 $this->bcIterator = new ArrayIterator( [] );
230 }
231 return $this->bcIterator;
232 }
233
234 /**
235 * Frees the result set, if applicable.
236 */
237 function free() {
238 // ...
239 }
240
241 /**
242 * Did the search contain search syntax? If so, Special:Search won't offer
243 * the user a link to a create a page named by the search string because the
244 * name would contain the search syntax.
245 * @return bool
246 */
247 public function searchContainedSyntax() {
248 return $this->containedSyntax;
249 }
250
251 /**
252 * @return bool True when there are more pages of search results available.
253 */
254 public function hasMoreResults() {
255 return $this->hasMoreResults;
256 }
257
258 /**
259 * @param int $limit Shrink result set to $limit and flag
260 * if more results are available.
261 */
262 public function shrink( $limit ) {
263 if ( $this->count() > $limit ) {
264 $this->hasMoreResults = true;
265 // shrinking result set for implementations that
266 // have not implemented extractResults and use
267 // the default cache location. Other implementations
268 // must override this as well.
269 if ( is_array( $this->results ) ) {
270 $this->results = array_slice( $this->results, 0, $limit );
271 } else {
272 throw new \UnexpectedValueException(
273 "When overriding result store extending classes must "
274 . " also override " . __METHOD__ );
275 }
276 }
277 }
278
279 /**
280 * Extract all the results in the result set as array.
281 * @return SearchResult[]
282 */
283 public function extractResults() {
284 if ( is_null( $this->results ) ) {
285 $this->results = [];
286 if ( $this->numRows() == 0 ) {
287 // Don't bother if we've got empty result
288 return $this->results;
289 }
290 $this->rewind();
291 while ( ( $result = $this->next() ) != false ) {
292 $this->results[] = $result;
293 }
294 $this->rewind();
295 }
296 return $this->results;
297 }
298
299 /**
300 * Extract all the titles in the result set.
301 * @return Title[]
302 */
303 public function extractTitles() {
304 if ( is_null( $this->titles ) ) {
305 if ( $this->numRows() == 0 ) {
306 // Don't bother if we've got empty result
307 $this->titles = [];
308 } else {
309 $this->titles = array_map(
310 function ( SearchResult $result ) {
311 return $result->getTitle();
312 },
313 $this->extractResults() );
314 }
315 }
316 return $this->titles;
317 }
318
319 /**
320 * Sets augmented data for result set.
321 * @param string $name Extra data item name
322 * @param array[] $data Extra data as PAGEID => data
323 */
324 public function setAugmentedData( $name, $data ) {
325 foreach ( $data as $id => $resultData ) {
326 $this->extraData[$id][$name] = $resultData;
327 }
328 }
329
330 /**
331 * Returns extra data for specific result and store it in SearchResult object.
332 * @param SearchResult $result
333 */
334 public function augmentResult( SearchResult $result ) {
335 $id = $result->getTitle()->getArticleID();
336 if ( $id === -1 ) {
337 return;
338 }
339 $result->setExtensionData( function () use ( $id ) {
340 return $this->extraData[$id] ?? [];
341 } );
342 }
343
344 /**
345 * @return int|null The offset the current page starts at. Typically
346 * this should be null to allow the UI to decide on its own, but in
347 * special cases like interleaved AB tests specifying explicitly is
348 * necessary.
349 */
350 public function getOffset() {
351 return null;
352 }
353
354 final public function getIterator() {
355 return new ArrayIterator( $this->extractResults() );
356 }
357 }