]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/specials/SpecialSearch.php
MediaWiki 1.16.1
[autoinstalls/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;
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         $searchPage = new SpecialSearch( $wgRequest, $wgUser );
39         if( $wgRequest->getVal( 'fulltext' )
40                 || !is_null( $wgRequest->getVal( 'offset' ))
41                 || !is_null( $wgRequest->getVal( 'searchx' )) )
42         {
43                 $searchPage->showResults( $search );
44         } else {
45                 $searchPage->goResult( $search );
46         }
47 }
48
49 /**
50  * implements Special:Search - Run text & title search and display the output
51  * @ingroup SpecialPage
52  */
53 class SpecialSearch {
54
55         /**
56          * Set up basic search parameters from the request and user settings.
57          * Typically you'll pass $wgRequest and $wgUser.
58          *
59          * @param WebRequest $request
60          * @param User $user
61          * @public
62          */
63         function __construct( &$request, &$user ) {
64                 list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
65                 $this->mPrefix = $request->getVal('prefix', '');
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                 $this->didYouMeanHtml = ''; # html of did you mean... link
76                 $this->fulltext = $request->getVal('fulltext');
77         }
78
79         /**
80          * If an exact title match can be found, jump straight ahead to it.
81          * @param string $term
82          */
83         public function goResult( $term ) {
84                 global $wgOut;
85                 $this->setupPage( $term );
86                 # Try to go to page as entered.
87                 $t = Title::newFromText( $term );
88                 # If the string cannot be used to create a title
89                 if( is_null( $t ) ) {
90                         return $this->showResults( $term );
91                 }
92                 # If there's an exact or very near match, jump right there.
93                 $t = SearchEngine::getNearMatch( $term );
94                 if( !is_null( $t ) ) {
95                         $wgOut->redirect( $t->getFullURL() );
96                         return;
97                 }
98                 # No match, generate an edit URL
99                 $t = Title::newFromText( $term );
100                 if( !is_null( $t ) ) {
101                         global $wgGoToEdit;
102                         wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
103                         # If the feature is enabled, go straight to the edit page
104                         if( $wgGoToEdit ) {
105                                 $wgOut->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) );
106                                 return;
107                         }
108                 }
109                 return $this->showResults( $term );
110         }
111
112         /**
113          * @param string $term
114          */
115         public function showResults( $term ) {
116                 global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang, $wgScript;
117                 wfProfileIn( __METHOD__ );
118
119                 $sk = $wgUser->getSkin();
120
121                 $this->searchEngine = SearchEngine::create();
122                 $search =& $this->searchEngine;
123                 $search->setLimitOffset( $this->limit, $this->offset );
124                 $search->setNamespaces( $this->namespaces );
125                 $search->showRedirects = $this->searchRedirects;
126                 $search->prefix = $this->mPrefix;
127                 $term = $search->transformSearchTerm($term);
128
129                 $this->setupPage( $term );
130
131                 if( $wgDisableTextSearch ) {
132                         global $wgSearchForwardUrl;
133                         if( $wgSearchForwardUrl ) {
134                                 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
135                                 $wgOut->redirect( $url );
136                                 wfProfileOut( __METHOD__ );
137                                 return;
138                         }
139                         global $wgInputEncoding;
140                         $wgOut->addHTML(
141                                 Xml::openElement( 'fieldset' ) .
142                                 Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
143                                 Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
144                                 wfMsg( 'googlesearch',
145                                         htmlspecialchars( $term ),
146                                         htmlspecialchars( $wgInputEncoding ),
147                                         htmlspecialchars( wfMsg( 'searchbutton' ) )
148                                 ) .
149                                 Xml::closeElement( 'fieldset' )
150                         );
151                         wfProfileOut( __METHOD__ );
152                         return;
153                 }
154
155                 $t = Title::newFromText( $term );
156
157                 // fetch search results
158                 $rewritten = $search->replacePrefixes($term);
159
160                 $titleMatches = $search->searchTitle( $rewritten );
161                 if( !($titleMatches instanceof SearchResultTooMany))
162                         $textMatches = $search->searchText( $rewritten );
163
164                 // did you mean... suggestions
165                 if( $textMatches && $textMatches->hasSuggestion() ) {
166                         $st = SpecialPage::getTitleFor( 'Search' );
167
168                         # mirror Go/Search behaviour of original request ..
169                         $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
170
171                         if($this->fulltext != null)
172                                 $didYouMeanParams['fulltext'] = $this->fulltext;
173
174                         $stParams = array_merge(
175                                 $didYouMeanParams,
176                                 $this->powerSearchOptions()
177                         );
178
179                         $suggestionSnippet = $textMatches->getSuggestionSnippet();
180
181                         if( $suggestionSnippet == '' )
182                                 $suggestionSnippet = null;
183
184                         $suggestLink = $sk->linkKnown(
185                                 $st,
186                                 $suggestionSnippet,
187                                 array(),
188                                 $stParams
189                         );
190
191                         $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>';
192                 }
193                 // start rendering the page
194                 $wgOut->addHtml(
195                         Xml::openElement(
196                                 'form',
197                                 array(
198                                         'id' => ( $this->searchAdvanced ? 'powersearch' : 'search' ),
199                                         'method' => 'get',
200                                         'action' => $wgScript
201                                 )
202                         )
203                 );
204                 $wgOut->addHtml(
205                         Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
206                         Xml::openElement( 'tr' ) .
207                         Xml::openElement( 'td' ) . "\n" .
208                         $this->shortDialog( $term ) .
209                         Xml::closeElement('td') .
210                         Xml::closeElement('tr') .
211                         Xml::closeElement('table')
212                 );
213
214                 // Sometimes the search engine knows there are too many hits
215                 if( $titleMatches instanceof SearchResultTooMany ) {
216                         $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
217                         wfProfileOut( __METHOD__ );
218                         return;
219                 }
220
221                 $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':';
222                 if( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
223                         $wgOut->addHTML( $this->searchFocus() );
224                         $wgOut->addHTML( $this->formHeader($term, 0, 0));
225                         if( $this->searchAdvanced ) {
226                                 $wgOut->addHTML( $this->powerSearchBox( $term ) );
227                         } 
228                         $wgOut->addHTML( '</form>' );
229                         // Empty query -- straight view of search form
230                         wfProfileOut( __METHOD__ );
231                         return;
232                 }
233
234                 // Get number of results
235                 $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0;
236                 $textMatchesNum = $textMatches ? $textMatches->numRows() : 0;
237                 // Total initial query matches (possible false positives)
238                 $num = $titleMatchesNum + $textMatchesNum;
239                 
240                 // Get total actual results (after second filtering, if any)
241                 $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ?
242                         $titleMatches->getTotalHits() : $titleMatchesNum;
243                 $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ?
244                         $textMatches->getTotalHits() : $textMatchesNum;
245
246                 // get total number of results if backend can calculate it
247                 $totalRes = 0;
248                 if($titleMatches && !is_null( $titleMatches->getTotalHits() ) )
249                         $totalRes += $titleMatches->getTotalHits();
250                 if($textMatches && !is_null( $textMatches->getTotalHits() ))
251                         $totalRes += $textMatches->getTotalHits();
252                         
253                 // show number of results and current offset
254                 $wgOut->addHTML( $this->formHeader($term, $num, $totalRes));
255                 if( $this->searchAdvanced ) {
256                         $wgOut->addHTML( $this->powerSearchBox( $term ) );
257                 }
258                 
259                 $wgOut->addHtml( Xml::closeElement( 'form' ) );
260                 $wgOut->addHtml( "<div class='searchresults'>" );
261
262                 // prev/next links
263                 if( $num || $this->offset ) {
264                         // Show the create link ahead
265                         $this->showCreateLink( $t );
266                         $prevnext = wfViewPrevNext( $this->offset, $this->limit,
267                                 SpecialPage::getTitleFor( 'Search' ),
268                                 wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ),
269                                 max( $titleMatchesNum, $textMatchesNum ) < $this->limit
270                         );
271                         //$wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
272                         wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
273                 } else {
274                         wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
275                 } 
276
277                 if( $titleMatches ) {
278                         if( $numTitleMatches > 0 ) {
279                                 $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
280                                 $wgOut->addHTML( $this->showMatches( $titleMatches ) );
281                         }
282                         $titleMatches->free();
283                 }
284                 if( $textMatches ) {
285                         // output appropriate heading
286                         if( $numTextMatches > 0 && $numTitleMatches > 0 ) {
287                                 // if no title matches the heading is redundant
288                                 $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
289                         } elseif( $totalRes == 0 ) {
290                                 # Don't show the 'no text matches' if we received title matches
291                                 # $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
292                         }
293                         // show interwiki results if any
294                         if( $textMatches->hasInterwikiResults() ) {
295                                 $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
296                         }
297                         // show results
298                         if( $numTextMatches > 0 ) {
299                                 $wgOut->addHTML( $this->showMatches( $textMatches ) );
300                         }
301
302                         $textMatches->free();
303                 }
304                 if( $num === 0 ) {
305                         $wgOut->addWikiMsg( 'search-nonefound', wfEscapeWikiText( $term ) );
306                         $this->showCreateLink( $t );
307                 }
308                 $wgOut->addHtml( "</div>" );
309                 if( $num === 0 ) {
310                         $wgOut->addHTML( $this->searchFocus() );
311                 }
312
313                 if( $num || $this->offset ) {
314                         $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
315                 }
316                 wfProfileOut( __METHOD__ );
317         }
318         
319         protected function showCreateLink( $t ) {
320                 global $wgOut;
321                 
322                 // show direct page/create link if applicable
323                 $messageName = null;
324                 if( !is_null($t) ) {
325                         if( $t->isKnown() ) {
326                                 $messageName = 'searchmenu-exists';
327                         } elseif( $t->userCan( 'create' ) ) {
328                                 $messageName = 'searchmenu-new';
329                         }
330                 } 
331                 if( $messageName ) {
332                         $wgOut->addWikiMsg( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) );
333                 } else {
334                         // preserve the paragraph for margins etc...
335                         $wgOut->addHtml( '<p></p>' );
336                 }
337         }
338
339         /**
340          *
341          */
342         protected function setupPage( $term ) {
343                 global $wgOut;
344                 // Figure out the active search profile header
345                 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
346                 if( $this->searchAdvanced )
347                         $this->active = 'advanced';
348                 else {
349                         $profiles = $this->getSearchProfiles();
350                         
351                         foreach( $profiles as $key => $data ) {
352                                 if ( $this->namespaces == $data['namespaces'] && $key != 'advanced')
353                                         $this->active = $key;
354                         }
355                         
356                 }
357                 # Should advanced UI be used?
358                 $this->searchAdvanced = ($this->active === 'advanced');
359                 if( !empty( $term ) ) {
360                         $wgOut->setPageTitle( wfMsg( 'searchresults') );
361                         $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) );
362                 }
363                 $wgOut->setArticleRelated( false );
364                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
365                 // add javascript specific to special:search
366                 $wgOut->addScriptFile( 'search.js' );
367                 $wgOut->allowClickjacking();
368         }
369
370         /**
371          * Extract "power search" namespace settings from the request object,
372          * returning a list of index numbers to search.
373          *
374          * @param WebRequest $request
375          * @return array
376          */
377         protected function powerSearch( &$request ) {
378                 $arr = array();
379                 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
380                         if( $request->getCheck( 'ns' . $ns ) ) {
381                                 $arr[] = $ns;
382                         }
383                 }
384                 return $arr;
385         }
386
387         /**
388          * Reconstruct the 'power search' options for links
389          * @return array
390          */
391         protected function powerSearchOptions() {
392                 $opt = array();
393                 foreach( $this->namespaces as $n ) {
394                         $opt['ns' . $n] = 1;
395                 }
396                 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
397                 if( $this->searchAdvanced ) {
398                         $opt['advanced'] = $this->searchAdvanced;
399                 }
400                 return $opt;
401         }
402
403         /**
404          * Show whole set of results
405          *
406          * @param SearchResultSet $matches
407          */
408         protected function showMatches( &$matches ) {
409                 global $wgContLang;
410                 wfProfileIn( __METHOD__ );
411
412                 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
413
414                 $out = "";
415                 $infoLine = $matches->getInfo();
416                 if( !is_null($infoLine) ) {
417                         $out .= "\n<!-- {$infoLine} -->\n";
418                 }
419                 $off = $this->offset + 1;
420                 $out .= "<ul class='mw-search-results'>\n";
421                 while( $result = $matches->next() ) {
422                         $out .= $this->showHit( $result, $terms );
423                 }
424                 $out .= "</ul>\n";
425
426                 // convert the whole thing to desired language variant
427                 $out = $wgContLang->convert( $out );
428                 wfProfileOut( __METHOD__ );
429                 return $out;
430         }
431
432         /**
433          * Format a single hit result
434          * @param SearchResult $result
435          * @param array $terms terms to highlight
436          */
437         protected function showHit( $result, $terms ) {
438                 global $wgContLang, $wgLang, $wgUser;
439                 wfProfileIn( __METHOD__ );
440
441                 if( $result->isBrokenTitle() ) {
442                         wfProfileOut( __METHOD__ );
443                         return "<!-- Broken link in search result -->\n";
444                 }
445
446                 $sk = $wgUser->getSkin();
447                 $t = $result->getTitle();
448
449                 $titleSnippet = $result->getTitleSnippet($terms);
450
451                 if( $titleSnippet == '' )
452                         $titleSnippet = null;
453                 
454                 $link_t = clone $t;
455                 
456                 wfRunHooks( 'ShowSearchHitTitle',
457                                         array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
458
459                 $link = $this->sk->linkKnown(
460                         $link_t,
461                         $titleSnippet
462                 );
463
464                 //If page content is not readable, just return the title.
465                 //This is not quite safe, but better than showing excerpts from non-readable pages
466                 //Note that hiding the entry entirely would screw up paging.
467                 if( !$t->userCanRead() ) {
468                         wfProfileOut( __METHOD__ );
469                         return "<li>{$link}</li>\n";
470                 }
471
472                 // If the page doesn't *exist*... our search index is out of date.
473                 // The least confusing at this point is to drop the result.
474                 // You may get less results, but... oh well. :P
475                 if( $result->isMissingRevision() ) {
476                         wfProfileOut( __METHOD__ );
477                         return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
478                 }
479
480                 // format redirects / relevant sections
481                 $redirectTitle = $result->getRedirectTitle();
482                 $redirectText = $result->getRedirectSnippet($terms);
483                 $sectionTitle = $result->getSectionTitle();
484                 $sectionText = $result->getSectionSnippet($terms);
485                 $redirect = '';
486
487                 if( !is_null($redirectTitle) ) {
488                         if( $redirectText == '' )
489                                 $redirectText = null;
490
491                         $redirect = "<span class='searchalttitle'>" .
492                                 wfMsg(
493                                         'search-redirect',
494                                         $this->sk->linkKnown(
495                                                 $redirectTitle,
496                                                 $redirectText
497                                         )
498                                 ) .
499                                 "</span>";
500                 }
501
502                 $section = '';
503
504
505                 if( !is_null($sectionTitle) ) {
506                         if( $sectionText == '' )
507                                 $sectionText = null;
508
509                         $section = "<span class='searchalttitle'>" .
510                                 wfMsg(
511                                         'search-section', $this->sk->linkKnown(
512                                                 $sectionTitle,
513                                                 $sectionText
514                                         )
515                                 ) .
516                                 "</span>";
517                 }
518
519                 // format text extract
520                 $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
521
522                 // format score
523                 if( is_null( $result->getScore() ) ) {
524                         // Search engine doesn't report scoring info
525                         $score = '';
526                 } else {
527                         $percent = sprintf( '%2.1f', $result->getScore() * 100 );
528                         $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
529                                 . ' - ';
530                 }
531
532                 // format description
533                 $byteSize = $result->getByteSize();
534                 $wordCount = $result->getWordCount();
535                 $timestamp = $result->getTimestamp();
536                 $size = wfMsgExt(
537                         'search-result-size',
538                         array( 'parsemag', 'escape' ),
539                         $this->sk->formatSize( $byteSize ),
540                         $wgLang->formatNum( $wordCount )
541                 );
542                 $date = $wgLang->timeanddate( $timestamp );
543
544                 // link to related articles if supported
545                 $related = '';
546                 if( $result->hasRelated() ) {
547                         $st = SpecialPage::getTitleFor( 'Search' );
548                         $stParams = array_merge(
549                                 $this->powerSearchOptions(),
550                                 array(
551                                         'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(),
552                                         'fulltext' => wfMsg( 'search' )
553                                 )
554                         );
555
556                         $related = ' -- ' . $sk->linkKnown(
557                                 $st,
558                                 wfMsg('search-relatedarticle'),
559                                 array(),
560                                 $stParams
561                         );
562                 }
563
564                 // Include a thumbnail for media files...
565                 if( $t->getNamespace() == NS_FILE ) {
566                         $img = wfFindFile( $t );
567                         if( $img ) {
568                                 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
569                                 if( $thumb ) {
570                                         $desc = $img->getShortDesc();
571                                         wfProfileOut( __METHOD__ );
572                                         // Float doesn't seem to interact well with the bullets.
573                                         // Table messes up vertical alignment of the bullets.
574                                         // Bullets are therefore disabled (didn't look great anyway).
575                                         return "<li>" .
576                                                 '<table class="searchResultImage">' .
577                                                 '<tr>' .
578                                                 '<td width="120" align="center" valign="top">' .
579                                                 $thumb->toHtml( array( 'desc-link' => true ) ) .
580                                                 '</td>' .
581                                                 '<td valign="top">' .
582                                                 $link .
583                                                 $extract .
584                                                 "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
585                                                 '</td>' .
586                                                 '</tr>' .
587                                                 '</table>' .
588                                                 "</li>\n";
589                                 }
590                         }
591                 }
592
593                 wfProfileOut( __METHOD__ );
594                 return "<li>{$link} {$redirect} {$section} {$extract}\n" .
595                         "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
596                         "</li>\n";
597
598         }
599
600         /**
601          * Show results from other wikis
602          *
603          * @param SearchResultSet $matches
604          */
605         protected function showInterwiki( &$matches, $query ) {
606                 global $wgContLang;
607                 wfProfileIn( __METHOD__ );
608                 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
609
610                 $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
611                         wfMsg('search-interwiki-caption')."</div>\n";
612                 $off = $this->offset + 1;
613                 $out .= "<ul class='mw-search-iwresults'>\n";
614
615                 // work out custom project captions
616                 $customCaptions = array();
617                 $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
618                 foreach($customLines as $line) {
619                         $parts = explode(":",$line,2);
620                         if(count($parts) == 2) // validate line
621                                 $customCaptions[$parts[0]] = $parts[1];
622                 }
623
624                 $prev = null;
625                 while( $result = $matches->next() ) {
626                         $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
627                         $prev = $result->getInterwikiPrefix();
628                 }
629                 // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
630                 $out .= "</ul></div>\n";
631
632                 // convert the whole thing to desired language variant
633                 $out = $wgContLang->convert( $out );
634                 wfProfileOut( __METHOD__ );
635                 return $out;
636         }
637
638         /**
639          * Show single interwiki link
640          *
641          * @param SearchResult $result
642          * @param string $lastInterwiki
643          * @param array $terms
644          * @param string $query
645          * @param array $customCaptions iw prefix -> caption
646          */
647         protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
648                 wfProfileIn( __METHOD__ );
649                 global $wgContLang, $wgLang;
650
651                 if( $result->isBrokenTitle() ) {
652                         wfProfileOut( __METHOD__ );
653                         return "<!-- Broken link in search result -->\n";
654                 }
655
656                 $t = $result->getTitle();
657
658                 $titleSnippet = $result->getTitleSnippet($terms);
659
660                 if( $titleSnippet == '' )
661                         $titleSnippet = null;
662
663                 $link = $this->sk->linkKnown(
664                         $t,
665                         $titleSnippet
666                 );
667
668                 // format redirect if any
669                 $redirectTitle = $result->getRedirectTitle();
670                 $redirectText = $result->getRedirectSnippet($terms);
671                 $redirect = '';
672                 if( !is_null($redirectTitle) ) {
673                         if( $redirectText == '' )
674                                 $redirectText = null;
675
676                         $redirect = "<span class='searchalttitle'>" .
677                                 wfMsg(
678                                         'search-redirect',
679                                         $this->sk->linkKnown(
680                                                 $redirectTitle,
681                                                 $redirectText
682                                         )
683                                 ) .
684                                 "</span>";
685                 }
686
687                 $out = "";
688                 // display project name
689                 if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) {
690                         if( key_exists($t->getInterwiki(),$customCaptions) )
691                                 // captions from 'search-interwiki-custom'
692                                 $caption = $customCaptions[$t->getInterwiki()];
693                         else{
694                                 // default is to show the hostname of the other wiki which might suck
695                                 // if there are many wikis on one hostname
696                                 $parsed = parse_url($t->getFullURL());
697                                 $caption = wfMsg('search-interwiki-default', $parsed['host']);
698                         }
699                         // "more results" link (special page stuff could be localized, but we might not know target lang)
700                         $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
701                         $searchLink = $this->sk->linkKnown(
702                                 $searchTitle,
703                                 wfMsg('search-interwiki-more'),
704                                 array(),
705                                 array(
706                                         'search' => $query,
707                                         'fulltext' => 'Search'
708                                 )
709                         );
710                         $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
711                                 {$searchLink}</span>{$caption}</div>\n<ul>";
712                 }
713
714                 $out .= "<li>{$link} {$redirect}</li>\n";
715                 wfProfileOut( __METHOD__ );
716                 return $out;
717         }
718
719
720         /**
721          * Generates the power search box at bottom of [[Special:Search]]
722          * @param $term string: search term
723          * @return $out string: HTML form
724          */
725         protected function powerSearchBox( $term ) {
726                 global $wgScript, $wgContLang;
727                 
728                 // Groups namespaces into rows according to subject
729                 $rows = array();
730                 foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) {
731                         $subject = MWNamespace::getSubject( $namespace );
732                         if( !array_key_exists( $subject, $rows ) ) {
733                                 $rows[$subject] = "";
734                         }
735                         $name = str_replace( '_', ' ', $name );
736                         if( $name == '' ) {
737                                 $name = wfMsg( 'blanknamespace' );
738                         }
739                         $rows[$subject] .=
740                                 Xml::openElement(
741                                         'td', array( 'style' => 'white-space: nowrap' )
742                                 ) .
743                                 Xml::checkLabel(
744                                         $name,
745                                         "ns{$namespace}",
746                                         "mw-search-ns{$namespace}",
747                                         in_array( $namespace, $this->namespaces )
748                                 ) .
749                                 Xml::closeElement( 'td' );
750                 }
751                 $rows = array_values( $rows );
752                 $numRows = count( $rows );
753                 
754                 // Lays out namespaces in multiple floating two-column tables so they'll
755                 // be arranged nicely while still accommodating different screen widths
756                 $namespaceTables = '';
757                 for( $i = 0; $i < $numRows; $i += 4 ) {
758                         $namespaceTables .= Xml::openElement(
759                                 'table',
760                                 array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 )
761                         );
762                         for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
763                                 $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
764                         }
765                         $namespaceTables .= Xml::closeElement( 'table' );
766                 }
767                 // Show redirects check only if backend supports it
768                 $redirects = '';
769                 if( $this->searchEngine->acceptListRedirects() ) {
770                         $redirects =
771                                 Xml::check(
772                                         'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' )
773                                 ) .
774                                 ' ' .
775                                 Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
776                 }
777                 // Return final output
778                 return
779                         Xml::openElement(
780                                 'fieldset',
781                                 array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
782                         ) .
783                         Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
784                         Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) .
785                         Xml::tags(
786                                 'div',
787                                 array( 'id' => 'mw-search-togglebox' ),
788                                 Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) .
789                                         Xml::element(
790                                                 'input',
791                                                 array(
792                                                         'type'=>'button',
793                                                         'id' => 'mw-search-toggleall',
794                                                         'onclick' => 'mwToggleSearchCheckboxes("all");',
795                                                         'value' => wfMsg( 'powersearch-toggleall' )
796                                                 )
797                                         ) .
798                                         Xml::element(
799                                                 'input',
800                                                 array(
801                                                         'type'=>'button',
802                                                         'id' => 'mw-search-togglenone',
803                                                         'onclick' => 'mwToggleSearchCheckboxes("none");',
804                                                         'value' => wfMsg( 'powersearch-togglenone' )
805                                                 )
806                                         )
807                         ) .
808                         Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
809                         $namespaceTables .
810                         Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
811                         $redirects .
812                         Xml::hidden( 'title', SpecialPage::getTitleFor( 'Search' )->getPrefixedText() ) .
813                         Xml::hidden( 'advanced', $this->searchAdvanced ) .
814                         Xml::hidden( 'fulltext', 'Advanced search' ) .
815                         Xml::closeElement( 'fieldset' );
816         }
817
818         protected function searchFocus() {
819                 $id = $this->searchAdvanced ? 'powerSearchText' : 'searchText';
820                 return Html::inlineScript(
821                         "hookEvent(\"load\", function() {" .
822                                 "document.getElementById('$id').focus();" .
823                         "});" );
824         }
825         
826         protected function getSearchProfiles() {
827                 // Builds list of Search Types (profiles)
828                 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
829                 
830                 $profiles = array(
831                         'default' => array(
832                                 'message' => 'searchprofile-articles',
833                                 'tooltip' => 'searchprofile-articles-tooltip',
834                                 'namespaces' => SearchEngine::defaultNamespaces(),
835                                 'namespace-messages' => SearchEngine::namespacesAsText(
836                                         SearchEngine::defaultNamespaces()
837                                 ),
838                         ),
839                         'images' => array(
840                                 'message' => 'searchprofile-images',
841                                 'tooltip' => 'searchprofile-images-tooltip',
842                                 'namespaces' => array( NS_FILE ),
843                         ),
844                         'help' => array(
845                                 'message' => 'searchprofile-project',
846                                 'tooltip' => 'searchprofile-project-tooltip',
847                                 'namespaces' => SearchEngine::helpNamespaces(),
848                                 'namespace-messages' => SearchEngine::namespacesAsText(
849                                         SearchEngine::helpNamespaces()
850                                 ),
851                         ),
852                         'all' => array(
853                                 'message' => 'searchprofile-everything',
854                                 'tooltip' => 'searchprofile-everything-tooltip',
855                                 'namespaces' => $nsAllSet,
856                         ),
857                         'advanced' => array(
858                                 'message' => 'searchprofile-advanced',
859                                 'tooltip' => 'searchprofile-advanced-tooltip',
860                                 'namespaces' => $this->namespaces,
861                                 'parameters' => array( 'advanced' => 1 ),
862                         )
863                 );
864                 
865                 wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
866
867                 foreach( $profiles as $key => &$data ) {
868                         sort($data['namespaces']);
869                 }
870                 
871                 return $profiles;
872         }
873
874         protected function formHeader( $term, $resultsShown, $totalNum ) {
875                 global $wgContLang, $wgLang;
876                 
877                 $out = Xml::openElement('div', array( 'class' =>  'mw-search-formheader' ) );
878                 
879                 $bareterm = $term;
880                 if( $this->startsWithImage( $term ) ) {
881                         // Deletes prefixes
882                         $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
883                 }
884
885                 
886                 $profiles = $this->getSearchProfiles();
887                 
888                 // Outputs XML for Search Types
889                 $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
890                 $out .= Xml::openElement( 'ul' );
891                 foreach ( $profiles as $id => $profile ) {
892                         $tooltipParam = isset( $profile['namespace-messages'] ) ?
893                                 $wgLang->commaList( $profile['namespace-messages'] ) : null;
894                         $out .= Xml::tags(
895                                 'li',
896                                 array(
897                                         'class' => $this->active == $id ? 'current' : 'normal'
898                                 ),
899                                 $this->makeSearchLink(
900                                         $bareterm,
901                                         $profile['namespaces'],
902                                         wfMsg( $profile['message'] ),
903                                         wfMsg( $profile['tooltip'], $tooltipParam ),
904                                         isset( $profile['parameters'] ) ? $profile['parameters'] : array() 
905                                 )
906                         );
907                 }
908                 $out .= Xml::closeElement( 'ul' );
909                 $out .= Xml::closeElement('div') ;
910
911                 // Results-info
912                 if ( $resultsShown > 0 ) {
913                         if ( $totalNum > 0 ){
914                                 $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ),
915                                         $wgLang->formatNum( $this->offset + 1 ),
916                                         $wgLang->formatNum( $this->offset + $resultsShown ),
917                                         $wgLang->formatNum( $totalNum ),
918                                         wfEscapeWikiText( $term ),
919                                         $wgLang->formatNum( $resultsShown )
920                                 );
921                         } elseif ( $resultsShown >= $this->limit ) {
922                                 $top = wfShowingResults( $this->offset, $this->limit );
923                         } else {
924                                 $top = wfShowingResultsNum( $this->offset, $this->limit, $resultsShown );
925                         }
926                         $out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
927                                 Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
928                         );
929                 }
930                 
931                 $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
932                 $out .= Xml::closeElement('div');
933                 
934                 // Adds hidden namespace fields
935                 if ( !$this->searchAdvanced ) {
936                         foreach( $this->namespaces as $ns ) {
937                                 $out .= Xml::hidden( "ns{$ns}", '1' );
938                         }
939                 }
940                 
941                 return $out;
942         }
943
944         protected function shortDialog( $term ) {
945                 $searchTitle = SpecialPage::getTitleFor( 'Search' );
946                 $searchable = SearchEngine::searchableNamespaces();
947                 $out = Html::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
948                 // Keep redirect setting
949                 $out .= Html::hidden( "redirs", (int)$this->searchRedirects ) . "\n";
950                 // Term box
951                 $out .= Html::input( 'search', $term, 'search', array(
952                         'id' => $this->searchAdvanced ? 'powerSearchText' : 'searchText',
953                         'size' => '50',
954                         'autofocus'
955                 ) ) . "\n";
956                 $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
957                 $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n";
958                 return $out . $this->didYouMeanHtml;            
959         }
960
961         /** Make a search link with some target namespaces */
962         protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) {
963                 $opt = $params;
964                 foreach( $namespaces as $n ) {
965                         $opt['ns' . $n] = 1;
966                 }
967                 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
968
969                 $st = SpecialPage::getTitleFor( 'Search' );
970                 $stParams = array_merge(
971                         array(
972                                 'search' => $term,
973                                 'fulltext' => wfMsg( 'search' )
974                         ),
975                         $opt
976                 );
977
978                 return Xml::element(
979                         'a',
980                         array(
981                                 'href' => $st->getLocalURL( $stParams ),
982                                 'title' => $tooltip, 
983                                 'onmousedown' => 'mwSearchHeaderClick(this);',
984                                 'onkeydown' => 'mwSearchHeaderClick(this);'),
985                         $label
986                 );
987         }
988
989         /** Check if query starts with image: prefix */
990         protected function startsWithImage( $term ) {
991                 global $wgContLang;
992
993                 $p = explode( ':', $term );
994                 if( count( $p ) > 1 ) {
995                         return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
996                 }
997                 return false;
998         }
999         
1000         /** Check if query starts with all: prefix */
1001         protected function startsWithAll( $term ) {
1002
1003                 $allkeyword = wfMsgForContent('searchall');
1004                 
1005                 $p = explode( ':', $term );
1006                 if( count( $p ) > 1 ) {
1007                         return $p[0]  == $allkeyword;
1008                 }
1009                 return false;
1010         }
1011 }
1012