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