* Checkbox fix
[lhc/web/wiklou.git] / includes / specials / SpecialSearch.php
1 <?php
2 # Copyright (C) 2004 Brion Vibber <brion@pobox.com>
3 # http://www.mediawiki.org/
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 /**
21 * Run text & title search and display the output
22 * @file
23 * @ingroup SpecialPage
24 */
25
26 /**
27 * Entry point
28 *
29 * @param $par String: (default '')
30 */
31 function wfSpecialSearch( $par = '' ) {
32 global $wgRequest, $wgUser, $wgUseOldSearchUI;
33 // Strip underscores from title parameter; most of the time we'll want
34 // text form here. But don't strip underscores from actual text params!
35 $titleParam = str_replace( '_', ' ', $par );
36 // Fetch the search term
37 $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) );
38 $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch';
39 $searchPage = new $class( $wgRequest, $wgUser );
40 if( $wgRequest->getVal( 'fulltext' )
41 || !is_null( $wgRequest->getVal( 'offset' ))
42 || !is_null( $wgRequest->getVal( 'searchx' )) )
43 {
44 $searchPage->showResults( $search, 'search' );
45 } else {
46 $searchPage->goResult( $search );
47 }
48 }
49
50 /**
51 * implements Special:Search - Run text & title search and display the output
52 * @ingroup SpecialPage
53 */
54 class SpecialSearch {
55
56 /**
57 * Set up basic search parameters from the request and user settings.
58 * Typically you'll pass $wgRequest and $wgUser.
59 *
60 * @param WebRequest $request
61 * @param User $user
62 * @public
63 */
64 function __construct( &$request, &$user ) {
65 list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
66 # Extract requested namespaces
67 $this->namespaces = $this->powerSearch( $request );
68 if( empty( $this->namespaces ) ) {
69 $this->namespaces = SearchEngine::userNamespaces( $user );
70 }
71 $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
72 $this->searchAdvanced = $request->getVal( 'advanced' );
73 $this->active = 'advanced';
74 $this->sk = $user->getSkin();
75 }
76
77 /**
78 * If an exact title match can be found, jump straight ahead to it.
79 * @param string $term
80 */
81 public function goResult( $term ) {
82 global $wgOut;
83 $this->setupPage( $term );
84 # Try to go to page as entered.
85 $t = Title::newFromText( $term );
86 # If the string cannot be used to create a title
87 if( is_null( $t ) ) {
88 return $this->showResults( $term );
89 }
90 # If there's an exact or very near match, jump right there.
91 $t = SearchEngine::getNearMatch( $term );
92 if( !is_null( $t ) ) {
93 $wgOut->redirect( $t->getFullURL() );
94 return;
95 }
96 # No match, generate an edit URL
97 $t = Title::newFromText( $term );
98 if( !is_null( $t ) ) {
99 global $wgGoToEdit;
100 wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
101 # If the feature is enabled, go straight to the edit page
102 if( $wgGoToEdit ) {
103 $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
104 return;
105 }
106 }
107 return $this->showResults( $term );
108 }
109
110 /**
111 * @param string $term
112 */
113 public function showResults( $term ) {
114 global $wgOut, $wgDisableTextSearch;
115 wfProfileIn( __METHOD__ );
116
117 $this->setupPage( $term );
118 $this->searchEngine = SearchEngine::create();
119
120 $t = Title::newFromText( $term );
121
122 $wgOut->addHtml(
123 Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
124 Xml::openElement( 'tr' ) .
125 Xml::openElement( 'td' ) . "\n" .
126 ( $this->searchAdvanced ? $this->powerSearchBox( $term ) : $this->shortDialog( $term ) ) .
127 Xml::closeElement('td') .
128 Xml::closeElement('tr') .
129 Xml::closeElement('table')
130 );
131
132 if( '' === trim( $term ) ) {
133 // Empty query -- straight view of search form
134 wfProfileOut( __METHOD__ );
135 return;
136 }
137
138 if( $wgDisableTextSearch ) {
139 global $wgSearchForwardUrl;
140 if( $wgSearchForwardUrl ) {
141 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
142 $wgOut->redirect( $url );
143 wfProfileOut( __METHOD__ );
144 return;
145 }
146 global $wgInputEncoding;
147 $wgOut->addHTML(
148 Xml::openElement( 'fieldset' ) .
149 Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
150 Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
151 wfMsg( 'googlesearch',
152 htmlspecialchars( $term ),
153 htmlspecialchars( $wgInputEncoding ),
154 htmlspecialchars( wfMsg( 'searchbutton' ) )
155 ) .
156 Xml::closeElement( 'fieldset' )
157 );
158 wfProfileOut( __METHOD__ );
159 return;
160 }
161
162 $search =& $this->searchEngine;
163 $search->setLimitOffset( $this->limit, $this->offset );
164 $search->setNamespaces( $this->namespaces );
165 $search->showRedirects = $this->searchRedirects;
166 $rewritten = $search->replacePrefixes($term);
167
168 $titleMatches = $search->searchTitle( $rewritten );
169
170 // Sometimes the search engine knows there are too many hits
171 if( $titleMatches instanceof SearchResultTooMany ) {
172 $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
173 wfProfileOut( __METHOD__ );
174 return;
175 }
176
177 $textMatches = $search->searchText( $rewritten );
178
179 // did you mean... suggestions
180 if( $textMatches && $textMatches->hasSuggestion() ) {
181 $st = SpecialPage::getTitleFor( 'Search' );
182 $stParams = wfArrayToCGI(
183 array( 'search' => $textMatches->getSuggestionQuery(), 'fulltext' => wfMsg('search') ),
184 $this->powerSearchOptions()
185 );
186 $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
187 $textMatches->getSuggestionSnippet().'</a>';
188
189 $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
190 }
191
192 // show direct page/create link
193 if( !is_null($t) ) {
194 if( !$t->exists() ) {
195 $wgOut->addWikiMsg( 'searchmenu-new', wfEscapeWikiText( $t->getPrefixedText() ) );
196 } else {
197 $wgOut->addWikiMsg( 'searchmenu-exists', wfEscapeWikiText( $t->getPrefixedText() ) );
198 }
199 }
200
201 // show number of results
202 $numTitleMatches = $titleMatches ? $titleMatches->numRows() : 0;
203 $numTextMatches = $textMatches ? $textMatches->numRows() : 0;
204 $highestNum = max( $numTitleMatches, $numTextMatches );
205 // Total query matches (possible false positives)
206 $num = $numTitleMatches + $numTextMatches;
207 // Get total actual results
208 $totalNum = 0;
209 if( $titleMatches && !is_null($titleMatches->getTotalHits()) )
210 $totalNum += $titleMatches->getTotalHits();
211 if( $textMatches && !is_null($textMatches->getTotalHits()) )
212 $totalNum += $textMatches->getTotalHits();
213 if( $num > 0 ) {
214 if( $totalNum > 0 ) {
215 $top = wfMsgExt('showingresultstotal', array( 'parseinline' ),
216 $this->offset+1, $this->offset+$num, $totalNum, $num );
217 } elseif( $num >= $this->limit ) {
218 $top = wfShowingResults( $this->offset, $this->limit );
219 } else {
220 $top = wfShowingResultsNum( $this->offset, $this->limit, $num );
221 }
222 $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
223 }
224
225 // prev/next links
226 if( $num || $this->offset ) {
227 $prevnext = wfViewPrevNext( $this->offset, $this->limit,
228 SpecialPage::getTitleFor( 'Search' ),
229 wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ),
230 ($highestNum < $this->limit)
231 );
232 $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
233 wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
234 } else {
235 wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
236 }
237
238 $wgOut->addHtml( "<div class='searchresults'>" );
239 if( $titleMatches ) {
240 if( $numTitleMatches > 0 ) {
241 $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
242 $wgOut->addHTML( $this->showMatches( $titleMatches ) );
243 }
244 $titleMatches->free();
245 }
246 if( $textMatches ) {
247 // output appropriate heading
248 if( $numTextMatches > 0 && $numTitleMatches > 0 ) {
249 // if no title matches the heading is redundant
250 $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
251 } elseif( $num == 0 ) {
252 # Don't show the 'no text matches' if we received title matches
253 $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
254 }
255 // show interwiki results if any
256 if( $textMatches->hasInterwikiResults() )
257 $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
258 // show results
259 if( $numTextMatches > 0 )
260 $wgOut->addHTML( $this->showMatches( $textMatches ) );
261
262 $textMatches->free();
263 }
264 if( $totalNum == 0 ) {
265 $wgOut->addWikiMsg( 'search-nonefound' );
266 }
267 $wgOut->addHtml( "</div>" );
268
269 if( $num || $this->offset ) {
270 $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
271 }
272 wfProfileOut( __METHOD__ );
273 }
274
275 /**
276 *
277 */
278 protected function setupPage( $term ) {
279 global $wgOut;
280 // Figure out the active search profile header
281 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
282 if( $this->searchAdvanced )
283 $this->active = 'advanced';
284 else if( $this->namespaces === NS_IMAGE || $this->startsWithImage( $term ) )
285 $this->active = 'images';
286 elseif( $this->namespaces === $nsAllSet )
287 $this->active = 'all';
288 elseif( $this->namespaces === SearchEngine::defaultNamespaces() )
289 $this->active = 'default';
290 elseif( $this->namespaces === SearchEngine::defaultAndProjectNamespaces() )
291 $this->active = 'withproject';
292 elseif( $this->namespaces === SearchEngine::projectNamespaces() )
293 $this->active = 'project';
294 else
295 $this->active = 'advanced';
296 # Should advanced UI be used?
297 $this->searchAdvanced = ($this->active === 'advanced');
298 if( !empty( $term ) ) {
299 $wgOut->setPageTitle( wfMsg( 'searchresults') );
300 $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) );
301 }
302 $wgOut->setArticleRelated( false );
303 $wgOut->setRobotPolicy( 'noindex,nofollow' );
304 }
305
306 /**
307 * Extract "power search" namespace settings from the request object,
308 * returning a list of index numbers to search.
309 *
310 * @param WebRequest $request
311 * @return array
312 */
313 protected function powerSearch( &$request ) {
314 $arr = array();
315 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
316 if( $request->getCheck( 'ns' . $ns ) ) {
317 $arr[] = $ns;
318 }
319 }
320 return $arr;
321 }
322
323 /**
324 * Reconstruct the 'power search' options for links
325 * @return array
326 */
327 protected function powerSearchOptions() {
328 $opt = array();
329 foreach( $this->namespaces as $n ) {
330 $opt['ns' . $n] = 1;
331 }
332 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
333 if( $this->searchAdvanced ) {
334 $opt['advanced'] = $this->searchAdvanced;
335 }
336 return $opt;
337 }
338
339 /**
340 * Show whole set of results
341 *
342 * @param SearchResultSet $matches
343 */
344 protected function showMatches( &$matches ) {
345 global $wgContLang;
346 wfProfileIn( __METHOD__ );
347
348 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
349
350 $out = "";
351 $infoLine = $matches->getInfo();
352 if( !is_null($infoLine) ) {
353 $out .= "\n<!-- {$infoLine} -->\n";
354 }
355 $off = $this->offset + 1;
356 $out .= "<ul class='mw-search-results'>\n";
357 while( $result = $matches->next() ) {
358 $out .= $this->showHit( $result, $terms );
359 }
360 $out .= "</ul>\n";
361
362 // convert the whole thing to desired language variant
363 $out = $wgContLang->convert( $out );
364 wfProfileOut( __METHOD__ );
365 return $out;
366 }
367
368 /**
369 * Format a single hit result
370 * @param SearchResult $result
371 * @param array $terms terms to highlight
372 */
373 protected function showHit( $result, $terms ) {
374 global $wgContLang, $wgLang;
375 wfProfileIn( __METHOD__ );
376
377 if( $result->isBrokenTitle() ) {
378 wfProfileOut( __METHOD__ );
379 return "<!-- Broken link in search result -->\n";
380 }
381
382 $t = $result->getTitle();
383
384 $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
385
386 //If page content is not readable, just return the title.
387 //This is not quite safe, but better than showing excerpts from non-readable pages
388 //Note that hiding the entry entirely would screw up paging.
389 if( !$t->userCanRead() ) {
390 wfProfileOut( __METHOD__ );
391 return "<li>{$link}</li>\n";
392 }
393
394 // If the page doesn't *exist*... our search index is out of date.
395 // The least confusing at this point is to drop the result.
396 // You may get less results, but... oh well. :P
397 if( $result->isMissingRevision() ) {
398 wfProfileOut( __METHOD__ );
399 return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
400 }
401
402 // format redirects / relevant sections
403 $redirectTitle = $result->getRedirectTitle();
404 $redirectText = $result->getRedirectSnippet($terms);
405 $sectionTitle = $result->getSectionTitle();
406 $sectionText = $result->getSectionSnippet($terms);
407 $redirect = '';
408 if( !is_null($redirectTitle) )
409 $redirect = "<span class='searchalttitle'>"
410 .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText))
411 ."</span>";
412 $section = '';
413 if( !is_null($sectionTitle) )
414 $section = "<span class='searchalttitle'>"
415 .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText))
416 ."</span>";
417
418 // format text extract
419 $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
420
421 // format score
422 if( is_null( $result->getScore() ) ) {
423 // Search engine doesn't report scoring info
424 $score = '';
425 } else {
426 $percent = sprintf( '%2.1f', $result->getScore() * 100 );
427 $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
428 . ' - ';
429 }
430
431 // format description
432 $byteSize = $result->getByteSize();
433 $wordCount = $result->getWordCount();
434 $timestamp = $result->getTimestamp();
435 $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
436 $this->sk->formatSize( $byteSize ), $wordCount );
437 $date = $wgLang->timeanddate( $timestamp );
438
439 // link to related articles if supported
440 $related = '';
441 if( $result->hasRelated() ) {
442 $st = SpecialPage::getTitleFor( 'Search' );
443 $stParams = wfArrayToCGI( $this->powerSearchOptions(),
444 array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
445 'fulltext' => wfMsg('search') ));
446
447 $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'.
448 wfMsg('search-relatedarticle').'</a>';
449 }
450
451 // Include a thumbnail for media files...
452 if( $t->getNamespace() == NS_IMAGE ) {
453 $img = wfFindFile( $t );
454 if( $img ) {
455 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
456 if( $thumb ) {
457 $desc = $img->getShortDesc();
458 wfProfileOut( __METHOD__ );
459 // Float doesn't seem to interact well with the bullets.
460 // Table messes up vertical alignment of the bullets.
461 // Bullets are therefore disabled (didn't look great anyway).
462 return "<li>" .
463 '<table class="searchResultImage">' .
464 '<tr>' .
465 '<td width="120" align="center" valign="top">' .
466 $thumb->toHtml( array( 'desc-link' => true ) ) .
467 '</td>' .
468 '<td valign="top">' .
469 $link .
470 $extract .
471 "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
472 '</td>' .
473 '</tr>' .
474 '</table>' .
475 "</li>\n";
476 }
477 }
478 }
479
480 wfProfileOut( __METHOD__ );
481 return "<li>{$link} {$redirect} {$section} {$extract}\n" .
482 "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
483 "</li>\n";
484
485 }
486
487 /**
488 * Show results from other wikis
489 *
490 * @param SearchResultSet $matches
491 */
492 protected function showInterwiki( &$matches, $query ) {
493 global $wgContLang;
494 wfProfileIn( __METHOD__ );
495 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
496
497 $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
498 wfMsg('search-interwiki-caption')."</div>\n";
499 $off = $this->offset + 1;
500 $out .= "<ul start='{$off}' class='mw-search-iwresults'>\n";
501
502 // work out custom project captions
503 $customCaptions = array();
504 $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
505 foreach($customLines as $line) {
506 $parts = explode(":",$line,2);
507 if(count($parts) == 2) // validate line
508 $customCaptions[$parts[0]] = $parts[1];
509 }
510
511 $prev = null;
512 while( $result = $matches->next() ) {
513 $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
514 $prev = $result->getInterwikiPrefix();
515 }
516 // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
517 $out .= "</ul></div>\n";
518
519 // convert the whole thing to desired language variant
520 $out = $wgContLang->convert( $out );
521 wfProfileOut( __METHOD__ );
522 return $out;
523 }
524
525 /**
526 * Show single interwiki link
527 *
528 * @param SearchResult $result
529 * @param string $lastInterwiki
530 * @param array $terms
531 * @param string $query
532 * @param array $customCaptions iw prefix -> caption
533 */
534 protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
535 wfProfileIn( __METHOD__ );
536 global $wgContLang, $wgLang;
537
538 if( $result->isBrokenTitle() ) {
539 wfProfileOut( __METHOD__ );
540 return "<!-- Broken link in search result -->\n";
541 }
542
543 $t = $result->getTitle();
544
545 $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
546
547 // format redirect if any
548 $redirectTitle = $result->getRedirectTitle();
549 $redirectText = $result->getRedirectSnippet($terms);
550 $redirect = '';
551 if( !is_null($redirectTitle) )
552 $redirect = "<span class='searchalttitle'>"
553 .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText))
554 ."</span>";
555
556 $out = "";
557 // display project name
558 if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) {
559 if( key_exists($t->getInterwiki(),$customCaptions) )
560 // captions from 'search-interwiki-custom'
561 $caption = $customCaptions[$t->getInterwiki()];
562 else{
563 // default is to show the hostname of the other wiki which might suck
564 // if there are many wikis on one hostname
565 $parsed = parse_url($t->getFullURL());
566 $caption = wfMsg('search-interwiki-default', $parsed['host']);
567 }
568 // "more results" link (special page stuff could be localized, but we might not know target lang)
569 $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
570 $searchLink = $this->sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
571 wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search')));
572 $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
573 {$searchLink}</span>{$caption}</div>\n<ul>";
574 }
575
576 $out .= "<li>{$link} {$redirect}</li>\n";
577 wfProfileOut( __METHOD__ );
578 return $out;
579 }
580
581
582 /**
583 * Generates the power search box at bottom of [[Special:Search]]
584 * @param $term string: search term
585 * @return $out string: HTML form
586 */
587 protected function powerSearchBox( $term ) {
588 global $wgScript;
589
590 $namespaces = SearchEngine::searchableNamespaces();
591
592 $tables = $this->namespaceTables( $namespaces );
593
594 $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
595 $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
596 $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
597 $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n";
598 $searchTitle = SpecialPage::getTitleFor( 'Search' );
599
600 $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
601 Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" .
602 "<p>" .
603 wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
604 "</p>\n" .
605 $tables .
606 "<hr style=\"clear: both\" />\n" .
607 "<p>" .
608 $redirect . " " . $redirectLabel .
609 "</p>\n" .
610 wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
611 "&nbsp;" .
612 $searchField .
613 "&nbsp;" .
614 $searchButton .
615 "</form>";
616 $t = Title::newFromText( $term );
617 if( $t != null && count($this->namespaces) === 1 ) {
618 $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term );
619 }
620 return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) .
621 Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
622 $this->formHeader($term) . $out .
623 Xml::closeElement( 'fieldset' );
624 }
625
626 protected function powerSearchFocus() {
627 global $wgJsMimeType;
628 return "<script type=\"$wgJsMimeType\">" .
629 "hookEvent(\"load\", function() {" .
630 "document.getElementById('powerSearchText').focus();" .
631 "});" .
632 "</script>";
633 }
634
635 protected function formHeader( $term ) {
636 global $wgContLang, $wgCanonicalNamespaceNames;
637
638 $sep = '&nbsp;&nbsp;&nbsp;';
639 $out = Xml::openElement('div', array( 'style' => 'padding-bottom:0.5em;' ) );
640
641 $bareterm = $term;
642 if( $this->startsWithImage( $term ) )
643 $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); // delete all/image prefix
644
645 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
646
647 // search profiles headers
648 $m = wfMsg( 'searchprofile-articles' );
649 $tt = wfMsg( 'searchprofile-articles-tooltip',
650 implode( ', ', SearchEngine::namespacesAsText( SearchEngine::defaultNamespaces() ) ) );
651 if( $this->active == 'default' ) {
652 $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
653 } else {
654 $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultNamespaces(), $m, $tt );
655 }
656 $out .= $sep;
657
658 $m = wfMsg( 'searchprofile-images' );
659 $tt = wfMsg( 'searchprofile-images-tooltip' );
660 if( $this->active == 'images' ) {
661 $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
662 } else {
663 $imageTextForm = $wgContLang->getFormattedNsText(NS_IMAGE).':'.$bareterm;
664 $out .= $this->makeSearchLink( $imageTextForm, array( NS_IMAGE ) , $m, $tt );
665 }
666 $out .= $sep;
667
668 $m = wfMsg( 'searchprofile-articles-and-proj' );
669 $tt = wfMsg( 'searchprofile-project-tooltip',
670 implode( ', ', SearchEngine::namespacesAsText( SearchEngine::defaultAndProjectNamespaces() ) ) );
671 if( $this->active == 'withproject' ) {
672 $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
673 } else {
674 $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultAndProjectNamespaces(), $m, $tt );
675 }
676 $out .= $sep;
677
678 $m = wfMsg( 'searchprofile-project' );
679 $tt = wfMsg( 'searchprofile-project-tooltip',
680 implode( ', ', SearchEngine::namespacesAsText( SearchEngine::projectNamespaces() ) ) );
681 if( $this->active == 'project' ) {
682 $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
683 } else {
684 $out .= $this->makeSearchLink( $bareterm, SearchEngine::projectNamespaces(), $m, $tt );
685 }
686 $out .= $sep;
687
688 $m = wfMsg( 'searchprofile-everything' );
689 $tt = wfMsg( 'searchprofile-everything-tooltip' );
690 if( $this->active == 'all' ) {
691 $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
692 } else {
693 $out .= $this->makeSearchLink( $bareterm, $nsAllSet, $m, $tt );
694 }
695 $out .= $sep;
696
697 $m = wfMsg( 'searchprofile-advanced' );
698 $tt = wfMsg( 'searchprofile-advanced-tooltip' );
699 if( $this->active == 'advanced' ) {
700 $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
701 } else {
702 $out .= $this->makeSearchLink( $bareterm, $this->namespaces, $m, $tt, array( 'advanced' => '1' ) );
703 }
704 $out .= Xml::closeElement('div') ;
705
706 return $out;
707 }
708
709 protected function shortDialog( $term ) {
710 global $wgScript;
711 $searchTitle = SpecialPage::getTitleFor( 'Search' );
712 $searchable = SearchEngine::searchableNamespaces();
713 $out = Xml::openElement( 'form', array( 'id' => 'search', 'method' => 'get', 'action' => $wgScript ) );
714 $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
715 // If searching several, but not all namespaces, show what we are searching.
716 if( count($this->namespaces) > 1 && $this->namespaces !== array_keys($searchable) ) {
717 $active = array();
718 foreach( $this->namespaces as $ns ) {
719 $active[$ns] = $searchable[$ns];
720 }
721 $out .= wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . "<br/>\n";
722 $out .= $this->namespaceTables( $active, 1 )."<br/>\n";
723 }
724 $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . "\n";
725 $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
726 $out .= ' (' . wfMsgExt('searchmenu-help',array('parseinline') ) . ')';
727 $out .= Xml::closeElement( 'form' );
728 $t = Title::newFromText( $term );
729 if( $t != null && count($this->namespaces) === 1 ) {
730 $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term );
731 }
732 return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) .
733 Xml::element( 'legend', null, wfMsg('searchmenu-legend') ) .
734 $this->formHeader($term) . $out .
735 Xml::closeElement( 'fieldset' );
736 }
737
738 /** Make a search link with some target namespaces */
739 protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) {
740 $opt = $params;
741 foreach( $namespaces as $n ) {
742 $opt['ns' . $n] = 1;
743 }
744 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
745
746 $st = SpecialPage::getTitleFor( 'Search' );
747 $stParams = wfArrayToCGI( array( 'search' => $term, 'fulltext' => wfMsg( 'search' ) ), $opt );
748
749 return Xml::element( 'a',
750 array( 'href'=> $st->getLocalURL( $stParams ), 'title' => $tooltip ),
751 $label );
752 }
753
754 /** Check if query starts with image: prefix */
755 protected function startsWithImage( $term ) {
756 global $wgContLang;
757
758 $p = explode( ':', $term );
759 if( count( $p ) > 1 ) {
760 return $wgContLang->getNsIndex( $p[0] ) == NS_IMAGE;
761 }
762 return false;
763 }
764
765 protected function namespaceTables( $namespaces, $rowsPerTable = 3 ) {
766 global $wgContLang;
767 // Group namespaces into rows according to subject.
768 // Try not to make too many assumptions about namespace numbering.
769 $rows = array();
770 $tables = "";
771 foreach( $namespaces as $ns => $name ) {
772 $subj = Namespace::getSubject( $ns );
773 if( !array_key_exists( $subj, $rows ) ) {
774 $rows[$subj] = "";
775 }
776 $name = str_replace( '_', ' ', $name );
777 if( '' == $name ) {
778 $name = wfMsg( 'blanknamespace' );
779 }
780 $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) .
781 Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
782 Xml::closeElement( 'td' ) . "\n";
783 }
784 $rows = array_values( $rows );
785 $numRows = count( $rows );
786 // Lay out namespaces in multiple floating two-column tables so they'll
787 // be arranged nicely while still accommodating different screen widths
788 // Float to the right on RTL wikis
789 $tableStyle = $wgContLang->isRTL() ?
790 'float: right; margin: 0 0 0em 1em' : 'float: left; margin: 0 1em 0em 0';
791 // Build the final HTML table...
792 for( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
793 $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) );
794 for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
795 $tables .= "<tr>\n" . $rows[$j] . "</tr>";
796 }
797 $tables .= Xml::closeElement( 'table' ) . "\n";
798 }
799 return $tables;
800 }
801 }
802
803 /**
804 * implements Special:Search - Run text & title search and display the output
805 * @ingroup SpecialPage
806 */
807 class SpecialSearchOld {
808
809 /**
810 * Set up basic search parameters from the request and user settings.
811 * Typically you'll pass $wgRequest and $wgUser.
812 *
813 * @param WebRequest $request
814 * @param User $user
815 * @public
816 */
817 function __construct( &$request, &$user ) {
818 list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
819
820 $this->namespaces = $this->powerSearch( $request );
821 if( empty( $this->namespaces ) ) {
822 $this->namespaces = SearchEngine::userNamespaces( $user );
823 }
824
825 $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
826 }
827
828 /**
829 * If an exact title match can be found, jump straight ahead to it.
830 * @param string $term
831 * @public
832 */
833 function goResult( $term ) {
834 global $wgOut;
835 global $wgGoToEdit;
836
837 $this->setupPage( $term );
838
839 # Try to go to page as entered.
840 $t = Title::newFromText( $term );
841
842 # If the string cannot be used to create a title
843 if( is_null( $t ) ){
844 return $this->showResults( $term );
845 }
846
847 # If there's an exact or very near match, jump right there.
848 $t = SearchEngine::getNearMatch( $term );
849 if( !is_null( $t ) ) {
850 $wgOut->redirect( $t->getFullURL() );
851 return;
852 }
853
854 # No match, generate an edit URL
855 $t = Title::newFromText( $term );
856 if( ! is_null( $t ) ) {
857 wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
858 # If the feature is enabled, go straight to the edit page
859 if ( $wgGoToEdit ) {
860 $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
861 return;
862 }
863 }
864
865 $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' );
866 if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
867 $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) );
868 } else {
869 $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) );
870 }
871
872 return $this->showResults( $term );
873 }
874
875 /**
876 * @param string $term
877 * @public
878 */
879 function showResults( $term ) {
880 wfProfileIn( __METHOD__ );
881 global $wgOut, $wgUser;
882 $sk = $wgUser->getSkin();
883
884 $this->setupPage( $term );
885
886 $wgOut->addWikiMsg( 'searchresulttext' );
887
888 if( '' === trim( $term ) ) {
889 // Empty query -- straight view of search form
890 $wgOut->setSubtitle( '' );
891 $wgOut->addHTML( $this->powerSearchBox( $term ) );
892 $wgOut->addHTML( $this->powerSearchFocus() );
893 wfProfileOut( __METHOD__ );
894 return;
895 }
896
897 global $wgDisableTextSearch;
898 if ( $wgDisableTextSearch ) {
899 global $wgSearchForwardUrl;
900 if( $wgSearchForwardUrl ) {
901 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
902 $wgOut->redirect( $url );
903 wfProfileOut( __METHOD__ );
904 return;
905 }
906 global $wgInputEncoding;
907 $wgOut->addHTML(
908 Xml::openElement( 'fieldset' ) .
909 Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
910 Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
911 wfMsg( 'googlesearch',
912 htmlspecialchars( $term ),
913 htmlspecialchars( $wgInputEncoding ),
914 htmlspecialchars( wfMsg( 'searchbutton' ) )
915 ) .
916 Xml::closeElement( 'fieldset' )
917 );
918 wfProfileOut( __METHOD__ );
919 return;
920 }
921
922 $wgOut->addHTML( $this->shortDialog( $term ) );
923
924 $search = SearchEngine::create();
925 $search->setLimitOffset( $this->limit, $this->offset );
926 $search->setNamespaces( $this->namespaces );
927 $search->showRedirects = $this->searchRedirects;
928 $rewritten = $search->replacePrefixes($term);
929
930 $titleMatches = $search->searchTitle( $rewritten );
931
932 // Sometimes the search engine knows there are too many hits
933 if ($titleMatches instanceof SearchResultTooMany) {
934 $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
935 $wgOut->addHTML( $this->powerSearchBox( $term ) );
936 $wgOut->addHTML( $this->powerSearchFocus() );
937 wfProfileOut( __METHOD__ );
938 return;
939 }
940
941 $textMatches = $search->searchText( $rewritten );
942
943 // did you mean... suggestions
944 if($textMatches && $textMatches->hasSuggestion()){
945 $st = SpecialPage::getTitleFor( 'Search' );
946 $stParams = wfArrayToCGI( array(
947 'search' => $textMatches->getSuggestionQuery(),
948 'fulltext' => wfMsg('search')),
949 $this->powerSearchOptions());
950
951 $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
952 $textMatches->getSuggestionSnippet().'</a>';
953
954 $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
955 }
956
957 // show number of results
958 $num = ( $titleMatches ? $titleMatches->numRows() : 0 )
959 + ( $textMatches ? $textMatches->numRows() : 0);
960 $totalNum = 0;
961 if($titleMatches && !is_null($titleMatches->getTotalHits()))
962 $totalNum += $titleMatches->getTotalHits();
963 if($textMatches && !is_null($textMatches->getTotalHits()))
964 $totalNum += $textMatches->getTotalHits();
965 if ( $num > 0 ) {
966 if ( $totalNum > 0 ){
967 $top = wfMsgExt('showingresultstotal', array( 'parseinline' ),
968 $this->offset+1, $this->offset+$num, $totalNum, $num );
969 } elseif ( $num >= $this->limit ) {
970 $top = wfShowingResults( $this->offset, $this->limit );
971 } else {
972 $top = wfShowingResultsNum( $this->offset, $this->limit, $num );
973 }
974 $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
975 }
976
977 // prev/next links
978 if( $num || $this->offset ) {
979 $prevnext = wfViewPrevNext( $this->offset, $this->limit,
980 SpecialPage::getTitleFor( 'Search' ),
981 wfArrayToCGI(
982 $this->powerSearchOptions(),
983 array( 'search' => $term ) ),
984 ($num < $this->limit) );
985 $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
986 wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
987 } else {
988 wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
989 }
990
991 if( $titleMatches ) {
992 if( $titleMatches->numRows() ) {
993 $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
994 $wgOut->addHTML( $this->showMatches( $titleMatches ) );
995 }
996 $titleMatches->free();
997 }
998
999 if( $textMatches ) {
1000 // output appropriate heading
1001 if( $textMatches->numRows() ) {
1002 if($titleMatches)
1003 $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
1004 else // if no title matches the heading is redundant
1005 $wgOut->addHTML("<hr/>");
1006 } elseif( $num == 0 ) {
1007 # Don't show the 'no text matches' if we received title matches
1008 $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
1009 }
1010 // show interwiki results if any
1011 if( $textMatches->hasInterwikiResults() )
1012 $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
1013 // show results
1014 if( $textMatches->numRows() )
1015 $wgOut->addHTML( $this->showMatches( $textMatches ) );
1016
1017 $textMatches->free();
1018 }
1019
1020 if ( $num == 0 ) {
1021 $wgOut->addWikiMsg( 'nonefound' );
1022 }
1023 if( $num || $this->offset ) {
1024 $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
1025 }
1026 $wgOut->addHTML( $this->powerSearchBox( $term ) );
1027 wfProfileOut( __METHOD__ );
1028 }
1029
1030 #------------------------------------------------------------------
1031 # Private methods below this line
1032
1033 /**
1034 *
1035 */
1036 function setupPage( $term ) {
1037 global $wgOut;
1038 if( !empty( $term ) ){
1039 $wgOut->setPageTitle( wfMsg( 'searchresults') );
1040 $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term) ) );
1041 }
1042 $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
1043 $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
1044 $wgOut->setArticleRelated( false );
1045 $wgOut->setRobotPolicy( 'noindex,nofollow' );
1046 }
1047
1048 /**
1049 * Extract "power search" namespace settings from the request object,
1050 * returning a list of index numbers to search.
1051 *
1052 * @param WebRequest $request
1053 * @return array
1054 * @private
1055 */
1056 function powerSearch( &$request ) {
1057 $arr = array();
1058 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
1059 if( $request->getCheck( 'ns' . $ns ) ) {
1060 $arr[] = $ns;
1061 }
1062 }
1063 return $arr;
1064 }
1065
1066 /**
1067 * Reconstruct the 'power search' options for links
1068 * @return array
1069 * @private
1070 */
1071 function powerSearchOptions() {
1072 $opt = array();
1073 foreach( $this->namespaces as $n ) {
1074 $opt['ns' . $n] = 1;
1075 }
1076 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
1077 return $opt;
1078 }
1079
1080 /**
1081 * Show whole set of results
1082 *
1083 * @param SearchResultSet $matches
1084 */
1085 function showMatches( &$matches ) {
1086 wfProfileIn( __METHOD__ );
1087
1088 global $wgContLang;
1089 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
1090
1091 $out = "";
1092
1093 $infoLine = $matches->getInfo();
1094 if( !is_null($infoLine) )
1095 $out .= "\n<!-- {$infoLine} -->\n";
1096
1097
1098 $off = $this->offset + 1;
1099 $out .= "<ul class='mw-search-results'>\n";
1100
1101 while( $result = $matches->next() ) {
1102 $out .= $this->showHit( $result, $terms );
1103 }
1104 $out .= "</ul>\n";
1105
1106 // convert the whole thing to desired language variant
1107 global $wgContLang;
1108 $out = $wgContLang->convert( $out );
1109 wfProfileOut( __METHOD__ );
1110 return $out;
1111 }
1112
1113 /**
1114 * Format a single hit result
1115 * @param SearchResult $result
1116 * @param array $terms terms to highlight
1117 */
1118 function showHit( $result, $terms ) {
1119 wfProfileIn( __METHOD__ );
1120 global $wgUser, $wgContLang, $wgLang;
1121
1122 if( $result->isBrokenTitle() ) {
1123 wfProfileOut( __METHOD__ );
1124 return "<!-- Broken link in search result -->\n";
1125 }
1126
1127 $t = $result->getTitle();
1128 $sk = $wgUser->getSkin();
1129
1130 $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
1131
1132 //If page content is not readable, just return the title.
1133 //This is not quite safe, but better than showing excerpts from non-readable pages
1134 //Note that hiding the entry entirely would screw up paging.
1135 if (!$t->userCanRead()) {
1136 wfProfileOut( __METHOD__ );
1137 return "<li>{$link}</li>\n";
1138 }
1139
1140 // If the page doesn't *exist*... our search index is out of date.
1141 // The least confusing at this point is to drop the result.
1142 // You may get less results, but... oh well. :P
1143 if( $result->isMissingRevision() ) {
1144 wfProfileOut( __METHOD__ );
1145 return "<!-- missing page " .
1146 htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
1147 }
1148
1149 // format redirects / relevant sections
1150 $redirectTitle = $result->getRedirectTitle();
1151 $redirectText = $result->getRedirectSnippet($terms);
1152 $sectionTitle = $result->getSectionTitle();
1153 $sectionText = $result->getSectionSnippet($terms);
1154 $redirect = '';
1155 if( !is_null($redirectTitle) )
1156 $redirect = "<span class='searchalttitle'>"
1157 .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
1158 ."</span>";
1159 $section = '';
1160 if( !is_null($sectionTitle) )
1161 $section = "<span class='searchalttitle'>"
1162 .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
1163 ."</span>";
1164
1165 // format text extract
1166 $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
1167
1168 // format score
1169 if( is_null( $result->getScore() ) ) {
1170 // Search engine doesn't report scoring info
1171 $score = '';
1172 } else {
1173 $percent = sprintf( '%2.1f', $result->getScore() * 100 );
1174 $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
1175 . ' - ';
1176 }
1177
1178 // format description
1179 $byteSize = $result->getByteSize();
1180 $wordCount = $result->getWordCount();
1181 $timestamp = $result->getTimestamp();
1182 $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
1183 $sk->formatSize( $byteSize ),
1184 $wordCount );
1185 $date = $wgLang->timeanddate( $timestamp );
1186
1187 // link to related articles if supported
1188 $related = '';
1189 if( $result->hasRelated() ){
1190 $st = SpecialPage::getTitleFor( 'Search' );
1191 $stParams = wfArrayToCGI( $this->powerSearchOptions(),
1192 array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
1193 'fulltext' => wfMsg('search') ));
1194
1195 $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'.
1196 wfMsg('search-relatedarticle').'</a>';
1197 }
1198
1199 // Include a thumbnail for media files...
1200 if( $t->getNamespace() == NS_IMAGE ) {
1201 $img = wfFindFile( $t );
1202 if( $img ) {
1203 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
1204 if( $thumb ) {
1205 $desc = $img->getShortDesc();
1206 wfProfileOut( __METHOD__ );
1207 // Ugly table. :D
1208 // Float doesn't seem to interact well with the bullets.
1209 // Table messes up vertical alignment of the bullet, but I'm
1210 // not sure what more I can do about that. :(
1211 return "<li>" .
1212 '<table class="searchResultImage">' .
1213 '<tr>' .
1214 '<td width="120" align="center">' .
1215 $thumb->toHtml( array( 'desc-link' => true ) ) .
1216 '</td>' .
1217 '<td valign="top">' .
1218 $link .
1219 $extract .
1220 "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
1221 '</td>' .
1222 '</tr>' .
1223 '</table>' .
1224 "</li>\n";
1225 }
1226 }
1227 }
1228
1229 wfProfileOut( __METHOD__ );
1230 return "<li>{$link} {$redirect} {$section} {$extract}\n" .
1231 "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
1232 "</li>\n";
1233
1234 }
1235
1236 /**
1237 * Show results from other wikis
1238 *
1239 * @param SearchResultSet $matches
1240 */
1241 function showInterwiki( &$matches, $query ) {
1242 wfProfileIn( __METHOD__ );
1243
1244 global $wgContLang;
1245 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
1246
1247 $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".wfMsg('search-interwiki-caption')."</div>\n";
1248 $off = $this->offset + 1;
1249 $out .= "<ul start='{$off}' class='mw-search-iwresults'>\n";
1250
1251 // work out custom project captions
1252 $customCaptions = array();
1253 $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
1254 foreach($customLines as $line){
1255 $parts = explode(":",$line,2);
1256 if(count($parts) == 2) // validate line
1257 $customCaptions[$parts[0]] = $parts[1];
1258 }
1259
1260
1261 $prev = null;
1262 while( $result = $matches->next() ) {
1263 $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
1264 $prev = $result->getInterwikiPrefix();
1265 }
1266 // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
1267 $out .= "</ul></div>\n";
1268
1269 // convert the whole thing to desired language variant
1270 global $wgContLang;
1271 $out = $wgContLang->convert( $out );
1272 wfProfileOut( __METHOD__ );
1273 return $out;
1274 }
1275
1276 /**
1277 * Show single interwiki link
1278 *
1279 * @param SearchResult $result
1280 * @param string $lastInterwiki
1281 * @param array $terms
1282 * @param string $query
1283 * @param array $customCaptions iw prefix -> caption
1284 */
1285 function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
1286 wfProfileIn( __METHOD__ );
1287 global $wgUser, $wgContLang, $wgLang;
1288
1289 if( $result->isBrokenTitle() ) {
1290 wfProfileOut( __METHOD__ );
1291 return "<!-- Broken link in search result -->\n";
1292 }
1293
1294 $t = $result->getTitle();
1295 $sk = $wgUser->getSkin();
1296
1297 $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
1298
1299 // format redirect if any
1300 $redirectTitle = $result->getRedirectTitle();
1301 $redirectText = $result->getRedirectSnippet($terms);
1302 $redirect = '';
1303 if( !is_null($redirectTitle) )
1304 $redirect = "<span class='searchalttitle'>"
1305 .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
1306 ."</span>";
1307
1308 $out = "";
1309 // display project name
1310 if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
1311 if( key_exists($t->getInterwiki(),$customCaptions) )
1312 // captions from 'search-interwiki-custom'
1313 $caption = $customCaptions[$t->getInterwiki()];
1314 else{
1315 // default is to show the hostname of the other wiki which might suck
1316 // if there are many wikis on one hostname
1317 $parsed = parse_url($t->getFullURL());
1318 $caption = wfMsg('search-interwiki-default', $parsed['host']);
1319 }
1320 // "more results" link (special page stuff could be localized, but we might not know target lang)
1321 $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
1322 $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
1323 wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search')));
1324 $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>{$searchLink}</span>{$caption}</div>\n<ul>";
1325 }
1326
1327 $out .= "<li>{$link} {$redirect}</li>\n";
1328 wfProfileOut( __METHOD__ );
1329 return $out;
1330 }
1331
1332
1333 /**
1334 * Generates the power search box at bottom of [[Special:Search]]
1335 * @param $term string: search term
1336 * @return $out string: HTML form
1337 */
1338 function powerSearchBox( $term ) {
1339 global $wgScript, $wgContLang;
1340
1341 $namespaces = SearchEngine::searchableNamespaces();
1342
1343 // group namespaces into rows according to subject; try not to make too
1344 // many assumptions about namespace numbering
1345 $rows = array();
1346 foreach( $namespaces as $ns => $name ) {
1347 $subj = Namespace::getSubject( $ns );
1348 if( !array_key_exists( $subj, $rows ) ) {
1349 $rows[$subj] = "";
1350 }
1351 $name = str_replace( '_', ' ', $name );
1352 if( '' == $name ) {
1353 $name = wfMsg( 'blanknamespace' );
1354 }
1355 $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) .
1356 Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
1357 Xml::closeElement( 'td' ) . "\n";
1358 }
1359 $rows = array_values( $rows );
1360 $numRows = count( $rows );
1361
1362 // lay out namespaces in multiple floating two-column tables so they'll
1363 // be arranged nicely while still accommodating different screen widths
1364 $rowsPerTable = 3; // seems to look nice
1365
1366 // float to the right on RTL wikis
1367 $tableStyle = ( $wgContLang->isRTL() ?
1368 'float: right; margin: 0 0 1em 1em' :
1369 'float: left; margin: 0 1em 1em 0' );
1370
1371 $tables = "";
1372 for( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
1373 $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) );
1374 for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
1375 $tables .= "<tr>\n" . $rows[$j] . "</tr>";
1376 }
1377 $tables .= Xml::closeElement( 'table' ) . "\n";
1378 }
1379
1380 $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
1381 $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
1382 $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
1383 $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n";
1384 $searchTitle = SpecialPage::getTitleFor( 'Search' );
1385
1386 $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
1387 Xml::fieldset( wfMsg( 'powersearch-legend' ),
1388 Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" .
1389 "<p>" .
1390 wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
1391 "</p>\n" .
1392 $tables .
1393 "<hr style=\"clear: both\" />\n" .
1394 "<p>" .
1395 $redirect . " " . $redirectLabel .
1396 "</p>\n" .
1397 wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
1398 "&nbsp;" .
1399 $searchField .
1400 "&nbsp;" .
1401 $searchButton ) .
1402 "</form>";
1403
1404 return $out;
1405 }
1406
1407 function powerSearchFocus() {
1408 global $wgJsMimeType;
1409 return "<script type=\"$wgJsMimeType\">" .
1410 "hookEvent(\"load\", function(){" .
1411 "document.getElementById('powerSearchText').focus();" .
1412 "});" .
1413 "</script>";
1414 }
1415
1416 function shortDialog($term) {
1417 global $wgScript;
1418
1419 $out = Xml::openElement( 'form', array(
1420 'id' => 'search',
1421 'method' => 'get',
1422 'action' => $wgScript
1423 ));
1424 $searchTitle = SpecialPage::getTitleFor( 'Search' );
1425 $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() );
1426 $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
1427 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
1428 if( in_array( $ns, $this->namespaces ) ) {
1429 $out .= Xml::hidden( "ns{$ns}", '1' );
1430 }
1431 }
1432 $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
1433 $out .= Xml::closeElement( 'form' );
1434
1435 return $out;
1436 }
1437 }