SpecialSearch.php 49 KB

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