]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/specials/SpecialSearch.php
da054e02bd5dbb35ca4834644160863958e5f6ed
[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;
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         }
368
369         /**
370          * Extract "power search" namespace settings from the request object,
371          * returning a list of index numbers to search.
372          *
373          * @param WebRequest $request
374          * @return array
375          */
376         protected function powerSearch( &$request ) {
377                 $arr = array();
378                 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
379                         if( $request->getCheck( 'ns' . $ns ) ) {
380                                 $arr[] = $ns;
381                         }
382                 }
383                 return $arr;
384         }
385
386         /**
387          * Reconstruct the 'power search' options for links
388          * @return array
389          */
390         protected function powerSearchOptions() {
391                 $opt = array();
392                 foreach( $this->namespaces as $n ) {
393                         $opt['ns' . $n] = 1;
394                 }
395                 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
396                 if( $this->searchAdvanced ) {
397                         $opt['advanced'] = $this->searchAdvanced;
398                 }
399                 return $opt;
400         }
401
402         /**
403          * Show whole set of results
404          *
405          * @param SearchResultSet $matches
406          */
407         protected function showMatches( &$matches ) {
408                 global $wgContLang;
409                 wfProfileIn( __METHOD__ );
410
411                 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
412
413                 $out = "";
414                 $infoLine = $matches->getInfo();
415                 if( !is_null($infoLine) ) {
416                         $out .= "\n<!-- {$infoLine} -->\n";
417                 }
418                 $off = $this->offset + 1;
419                 $out .= "<ul class='mw-search-results'>\n";
420                 while( $result = $matches->next() ) {
421                         $out .= $this->showHit( $result, $terms );
422                 }
423                 $out .= "</ul>\n";
424
425                 // convert the whole thing to desired language variant
426                 $out = $wgContLang->convert( $out );
427                 wfProfileOut( __METHOD__ );
428                 return $out;
429         }
430
431         /**
432          * Format a single hit result
433          * @param SearchResult $result
434          * @param array $terms terms to highlight
435          */
436         protected function showHit( $result, $terms ) {
437                 global $wgContLang, $wgLang, $wgUser;
438                 wfProfileIn( __METHOD__ );
439
440                 if( $result->isBrokenTitle() ) {
441                         wfProfileOut( __METHOD__ );
442                         return "<!-- Broken link in search result -->\n";
443                 }
444
445                 $sk = $wgUser->getSkin();
446                 $t = $result->getTitle();
447
448                 $titleSnippet = $result->getTitleSnippet($terms);
449
450                 if( $titleSnippet == '' )
451                         $titleSnippet = null;
452                 
453                 $link_t = clone $t;
454                 
455                 wfRunHooks( 'ShowSearchHitTitle',
456                                         array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
457
458                 $link = $this->sk->linkKnown(
459                         $link_t,
460                         $titleSnippet
461                 );
462
463                 //If page content is not readable, just return the title.
464                 //This is not quite safe, but better than showing excerpts from non-readable pages
465                 //Note that hiding the entry entirely would screw up paging.
466                 if( !$t->userCanRead() ) {
467                         wfProfileOut( __METHOD__ );
468                         return "<li>{$link}</li>\n";
469                 }
470
471                 // If the page doesn't *exist*... our search index is out of date.
472                 // The least confusing at this point is to drop the result.
473                 // You may get less results, but... oh well. :P
474                 if( $result->isMissingRevision() ) {
475                         wfProfileOut( __METHOD__ );
476                         return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
477                 }
478
479                 // format redirects / relevant sections
480                 $redirectTitle = $result->getRedirectTitle();
481                 $redirectText = $result->getRedirectSnippet($terms);
482                 $sectionTitle = $result->getSectionTitle();
483                 $sectionText = $result->getSectionSnippet($terms);
484                 $redirect = '';
485
486                 if( !is_null($redirectTitle) ) {
487                         if( $redirectText == '' )
488                                 $redirectText = null;
489
490                         $redirect = "<span class='searchalttitle'>" .
491                                 wfMsg(
492                                         'search-redirect',
493                                         $this->sk->linkKnown(
494                                                 $redirectTitle,
495                                                 $redirectText
496                                         )
497                                 ) .
498                                 "</span>";
499                 }
500
501                 $section = '';
502
503
504                 if( !is_null($sectionTitle) ) {
505                         if( $sectionText == '' )
506                                 $sectionText = null;
507
508                         $section = "<span class='searchalttitle'>" .
509                                 wfMsg(
510                                         'search-section', $this->sk->linkKnown(
511                                                 $sectionTitle,
512                                                 $sectionText
513                                         )
514                                 ) .
515                                 "</span>";
516                 }
517
518                 // format text extract
519                 $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
520
521                 // format score
522                 if( is_null( $result->getScore() ) ) {
523                         // Search engine doesn't report scoring info
524                         $score = '';
525                 } else {
526                         $percent = sprintf( '%2.1f', $result->getScore() * 100 );
527                         $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
528                                 . ' - ';
529                 }
530
531                 // format description
532                 $byteSize = $result->getByteSize();
533                 $wordCount = $result->getWordCount();
534                 $timestamp = $result->getTimestamp();
535                 $size = wfMsgExt(
536                         'search-result-size',
537                         array( 'parsemag', 'escape' ),
538                         $this->sk->formatSize( $byteSize ),
539                         $wgLang->formatNum( $wordCount )
540                 );
541                 $date = $wgLang->timeanddate( $timestamp );
542
543                 // link to related articles if supported
544                 $related = '';
545                 if( $result->hasRelated() ) {
546                         $st = SpecialPage::getTitleFor( 'Search' );
547                         $stParams = array_merge(
548                                 $this->powerSearchOptions(),
549                                 array(
550                                         'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(),
551                                         'fulltext' => wfMsg( 'search' )
552                                 )
553                         );
554
555                         $related = ' -- ' . $sk->linkKnown(
556                                 $st,
557                                 wfMsg('search-relatedarticle'),
558                                 array(),
559                                 $stParams
560                         );
561                 }
562
563                 // Include a thumbnail for media files...
564                 if( $t->getNamespace() == NS_FILE ) {
565                         $img = wfFindFile( $t );
566                         if( $img ) {
567                                 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
568                                 if( $thumb ) {
569                                         $desc = $img->getShortDesc();
570                                         wfProfileOut( __METHOD__ );
571                                         // Float doesn't seem to interact well with the bullets.
572                                         // Table messes up vertical alignment of the bullets.
573                                         // Bullets are therefore disabled (didn't look great anyway).
574                                         return "<li>" .
575                                                 '<table class="searchResultImage">' .
576                                                 '<tr>' .
577                                                 '<td width="120" align="center" valign="top">' .
578                                                 $thumb->toHtml( array( 'desc-link' => true ) ) .
579                                                 '</td>' .
580                                                 '<td valign="top">' .
581                                                 $link .
582                                                 $extract .
583                                                 "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
584                                                 '</td>' .
585                                                 '</tr>' .
586                                                 '</table>' .
587                                                 "</li>\n";
588                                 }
589                         }
590                 }
591
592                 wfProfileOut( __METHOD__ );
593                 return "<li>{$link} {$redirect} {$section} {$extract}\n" .
594                         "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
595                         "</li>\n";
596
597         }
598
599         /**
600          * Show results from other wikis
601          *
602          * @param SearchResultSet $matches
603          */
604         protected function showInterwiki( &$matches, $query ) {
605                 global $wgContLang;
606                 wfProfileIn( __METHOD__ );
607                 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
608
609                 $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
610                         wfMsg('search-interwiki-caption')."</div>\n";
611                 $off = $this->offset + 1;
612                 $out .= "<ul class='mw-search-iwresults'>\n";
613
614                 // work out custom project captions
615                 $customCaptions = array();
616                 $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
617                 foreach($customLines as $line) {
618                         $parts = explode(":",$line,2);
619                         if(count($parts) == 2) // validate line
620                                 $customCaptions[$parts[0]] = $parts[1];
621                 }
622
623                 $prev = null;
624                 while( $result = $matches->next() ) {
625                         $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
626                         $prev = $result->getInterwikiPrefix();
627                 }
628                 // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
629                 $out .= "</ul></div>\n";
630
631                 // convert the whole thing to desired language variant
632                 $out = $wgContLang->convert( $out );
633                 wfProfileOut( __METHOD__ );
634                 return $out;
635         }
636
637         /**
638          * Show single interwiki link
639          *
640          * @param SearchResult $result
641          * @param string $lastInterwiki
642          * @param array $terms
643          * @param string $query
644          * @param array $customCaptions iw prefix -> caption
645          */
646         protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
647                 wfProfileIn( __METHOD__ );
648                 global $wgContLang, $wgLang;
649
650                 if( $result->isBrokenTitle() ) {
651                         wfProfileOut( __METHOD__ );
652                         return "<!-- Broken link in search result -->\n";
653                 }
654
655                 $t = $result->getTitle();
656
657                 $titleSnippet = $result->getTitleSnippet($terms);
658
659                 if( $titleSnippet == '' )
660                         $titleSnippet = null;
661
662                 $link = $this->sk->linkKnown(
663                         $t,
664                         $titleSnippet
665                 );
666
667                 // format redirect if any
668                 $redirectTitle = $result->getRedirectTitle();
669                 $redirectText = $result->getRedirectSnippet($terms);
670                 $redirect = '';
671                 if( !is_null($redirectTitle) ) {
672                         if( $redirectText == '' )
673                                 $redirectText = null;
674
675                         $redirect = "<span class='searchalttitle'>" .
676                                 wfMsg(
677                                         'search-redirect',
678                                         $this->sk->linkKnown(
679                                                 $redirectTitle,
680                                                 $redirectText
681                                         )
682                                 ) .
683                                 "</span>";
684                 }
685
686                 $out = "";
687                 // display project name
688                 if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) {
689                         if( key_exists($t->getInterwiki(),$customCaptions) )
690                                 // captions from 'search-interwiki-custom'
691                                 $caption = $customCaptions[$t->getInterwiki()];
692                         else{
693                                 // default is to show the hostname of the other wiki which might suck
694                                 // if there are many wikis on one hostname
695                                 $parsed = parse_url($t->getFullURL());
696                                 $caption = wfMsg('search-interwiki-default', $parsed['host']);
697                         }
698                         // "more results" link (special page stuff could be localized, but we might not know target lang)
699                         $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
700                         $searchLink = $this->sk->linkKnown(
701                                 $searchTitle,
702                                 wfMsg('search-interwiki-more'),
703                                 array(),
704                                 array(
705                                         'search' => $query,
706                                         'fulltext' => 'Search'
707                                 )
708                         );
709                         $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
710                                 {$searchLink}</span>{$caption}</div>\n<ul>";
711                 }
712
713                 $out .= "<li>{$link} {$redirect}</li>\n";
714                 wfProfileOut( __METHOD__ );
715                 return $out;
716         }
717
718
719         /**
720          * Generates the power search box at bottom of [[Special:Search]]
721          * @param $term string: search term
722          * @return $out string: HTML form
723          */
724         protected function powerSearchBox( $term ) {
725                 global $wgScript, $wgContLang;
726                 
727                 // Groups namespaces into rows according to subject
728                 $rows = array();
729                 foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) {
730                         $subject = MWNamespace::getSubject( $namespace );
731                         if( !array_key_exists( $subject, $rows ) ) {
732                                 $rows[$subject] = "";
733                         }
734                         $name = str_replace( '_', ' ', $name );
735                         if( $name == '' ) {
736                                 $name = wfMsg( 'blanknamespace' );
737                         }
738                         $rows[$subject] .=
739                                 Xml::openElement(
740                                         'td', array( 'style' => 'white-space: nowrap' )
741                                 ) .
742                                 Xml::checkLabel(
743                                         $name,
744                                         "ns{$namespace}",
745                                         "mw-search-ns{$namespace}",
746                                         in_array( $namespace, $this->namespaces )
747                                 ) .
748                                 Xml::closeElement( 'td' );
749                 }
750                 $rows = array_values( $rows );
751                 $numRows = count( $rows );
752                 
753                 // Lays out namespaces in multiple floating two-column tables so they'll
754                 // be arranged nicely while still accommodating different screen widths
755                 $namespaceTables = '';
756                 for( $i = 0; $i < $numRows; $i += 4 ) {
757                         $namespaceTables .= Xml::openElement(
758                                 'table',
759                                 array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 )
760                         );
761                         for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
762                                 $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
763                         }
764                         $namespaceTables .= Xml::closeElement( 'table' );
765                 }
766                 // Show redirects check only if backend supports it
767                 $redirects = '';
768                 if( $this->searchEngine->acceptListRedirects() ) {
769                         $redirects =
770                                 Xml::check(
771                                         'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' )
772                                 ) .
773                                 ' ' .
774                                 Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
775                 }
776                 // Return final output
777                 return
778                         Xml::openElement(
779                                 'fieldset',
780                                 array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
781                         ) .
782                         Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
783                         Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) .
784                         Xml::tags(
785                                 'div',
786                                 array( 'id' => 'mw-search-togglebox' ),
787                                 Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) .
788                                         Xml::element(
789                                                 'input',
790                                                 array(
791                                                         'type'=>'button',
792                                                         'id' => 'mw-search-toggleall',
793                                                         'onclick' => 'mwToggleSearchCheckboxes("all");',
794                                                         'value' => wfMsg( 'powersearch-toggleall' )
795                                                 )
796                                         ) .
797                                         Xml::element(
798                                                 'input',
799                                                 array(
800                                                         'type'=>'button',
801                                                         'id' => 'mw-search-togglenone',
802                                                         'onclick' => 'mwToggleSearchCheckboxes("none");',
803                                                         'value' => wfMsg( 'powersearch-togglenone' )
804                                                 )
805                                         )
806                         ) .
807                         Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
808                         $namespaceTables .
809                         Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
810                         $redirects .
811                         Xml::hidden( 'title', SpecialPage::getTitleFor( 'Search' )->getPrefixedText() ) .
812                         Xml::hidden( 'advanced', $this->searchAdvanced ) .
813                         Xml::hidden( 'fulltext', 'Advanced search' ) .
814                         Xml::closeElement( 'fieldset' );
815         }
816
817         protected function searchFocus() {
818                 $id = $this->searchAdvanced ? 'powerSearchText' : 'searchText';
819                 return Html::inlineScript(
820                         "hookEvent(\"load\", function() {" .
821                                 "document.getElementById('$id').focus();" .
822                         "});" );
823         }
824         
825         protected function getSearchProfiles() {
826                 // Builds list of Search Types (profiles)
827                 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
828                 
829                 $profiles = array(
830                         'default' => array(
831                                 'message' => 'searchprofile-articles',
832                                 'tooltip' => 'searchprofile-articles-tooltip',
833                                 'namespaces' => SearchEngine::defaultNamespaces(),
834                                 'namespace-messages' => SearchEngine::namespacesAsText(
835                                         SearchEngine::defaultNamespaces()
836                                 ),
837                         ),
838                         'images' => array(
839                                 'message' => 'searchprofile-images',
840                                 'tooltip' => 'searchprofile-images-tooltip',
841                                 'namespaces' => array( NS_FILE ),
842                         ),
843                         'help' => array(
844                                 'message' => 'searchprofile-project',
845                                 'tooltip' => 'searchprofile-project-tooltip',
846                                 'namespaces' => SearchEngine::helpNamespaces(),
847                                 'namespace-messages' => SearchEngine::namespacesAsText(
848                                         SearchEngine::helpNamespaces()
849                                 ),
850                         ),
851                         'all' => array(
852                                 'message' => 'searchprofile-everything',
853                                 'tooltip' => 'searchprofile-everything-tooltip',
854                                 'namespaces' => $nsAllSet,
855                         ),
856                         'advanced' => array(
857                                 'message' => 'searchprofile-advanced',
858                                 'tooltip' => 'searchprofile-advanced-tooltip',
859                                 'namespaces' => $this->namespaces,
860                                 'parameters' => array( 'advanced' => 1 ),
861                         )
862                 );
863                 
864                 wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
865
866                 foreach( $profiles as $key => &$data ) {
867                         sort($data['namespaces']);
868                 }
869                 
870                 return $profiles;
871         }
872
873         protected function formHeader( $term, $resultsShown, $totalNum ) {
874                 global $wgContLang, $wgLang;
875                 
876                 $out = Xml::openElement('div', array( 'class' =>  'mw-search-formheader' ) );
877                 
878                 $bareterm = $term;
879                 if( $this->startsWithImage( $term ) ) {
880                         // Deletes prefixes
881                         $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
882                 }
883
884                 
885                 $profiles = $this->getSearchProfiles();
886                 
887                 // Outputs XML for Search Types
888                 $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
889                 $out .= Xml::openElement( 'ul' );
890                 foreach ( $profiles as $id => $profile ) {
891                         $tooltipParam = isset( $profile['namespace-messages'] ) ?
892                                 $wgLang->commaList( $profile['namespace-messages'] ) : null;
893                         $out .= Xml::tags(
894                                 'li',
895                                 array(
896                                         'class' => $this->active == $id ? 'current' : 'normal'
897                                 ),
898                                 $this->makeSearchLink(
899                                         $bareterm,
900                                         $profile['namespaces'],
901                                         wfMsg( $profile['message'] ),
902                                         wfMsg( $profile['tooltip'], $tooltipParam ),
903                                         isset( $profile['parameters'] ) ? $profile['parameters'] : array() 
904                                 )
905                         );
906                 }
907                 $out .= Xml::closeElement( 'ul' );
908                 $out .= Xml::closeElement('div') ;
909
910                 // Results-info
911                 if ( $resultsShown > 0 ) {
912                         if ( $totalNum > 0 ){
913                                 $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ),
914                                         $wgLang->formatNum( $this->offset + 1 ),
915                                         $wgLang->formatNum( $this->offset + $resultsShown ),
916                                         $wgLang->formatNum( $totalNum ),
917                                         wfEscapeWikiText( $term ),
918                                         $wgLang->formatNum( $resultsShown )
919                                 );
920                         } elseif ( $resultsShown >= $this->limit ) {
921                                 $top = wfShowingResults( $this->offset, $this->limit );
922                         } else {
923                                 $top = wfShowingResultsNum( $this->offset, $this->limit, $resultsShown );
924                         }
925                         $out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
926                                 Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
927                         );
928                 }
929                 
930                 $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
931                 $out .= Xml::closeElement('div');
932                 
933                 // Adds hidden namespace fields
934                 if ( !$this->searchAdvanced ) {
935                         foreach( $this->namespaces as $ns ) {
936                                 $out .= Xml::hidden( "ns{$ns}", '1' );
937                         }
938                 }
939                 
940                 return $out;
941         }
942
943         protected function shortDialog( $term ) {
944                 $searchTitle = SpecialPage::getTitleFor( 'Search' );
945                 $searchable = SearchEngine::searchableNamespaces();
946                 $out = Html::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
947                 // Keep redirect setting
948                 $out .= Html::hidden( "redirs", (int)$this->searchRedirects ) . "\n";
949                 // Term box
950                 $out .= Html::input( 'search', $term, 'search', array(
951                         'id' => $this->searchAdvanced ? 'powerSearchText' : 'searchText',
952                         'size' => '50',
953                         'autofocus'
954                 ) ) . "\n";
955                 $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
956                 $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n";
957                 return $out . $this->didYouMeanHtml;            
958         }
959
960         /** Make a search link with some target namespaces */
961         protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) {
962                 $opt = $params;
963                 foreach( $namespaces as $n ) {
964                         $opt['ns' . $n] = 1;
965                 }
966                 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
967
968                 $st = SpecialPage::getTitleFor( 'Search' );
969                 $stParams = array_merge(
970                         array(
971                                 'search' => $term,
972                                 'fulltext' => wfMsg( 'search' )
973                         ),
974                         $opt
975                 );
976
977                 return Xml::element(
978                         'a',
979                         array(
980                                 'href' => $st->getLocalURL( $stParams ),
981                                 'title' => $tooltip, 
982                                 'onmousedown' => 'mwSearchHeaderClick(this);',
983                                 'onkeydown' => 'mwSearchHeaderClick(this);'),
984                         $label
985                 );
986         }
987
988         /** Check if query starts with image: prefix */
989         protected function startsWithImage( $term ) {
990                 global $wgContLang;
991
992                 $p = explode( ':', $term );
993                 if( count( $p ) > 1 ) {
994                         return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
995                 }
996                 return false;
997         }
998         
999         /** Check if query starts with all: prefix */
1000         protected function startsWithAll( $term ) {
1001
1002                 $allkeyword = wfMsgForContent('searchall');
1003                 
1004                 $p = explode( ':', $term );
1005                 if( count( $p ) > 1 ) {
1006                         return $p[0]  == $allkeyword;
1007                 }
1008                 return false;
1009         }
1010 }
1011