SearchFormWidget.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <?php
  2. namespace MediaWiki\Widget\Search;
  3. use Hooks;
  4. use Html;
  5. use MediaWiki\MediaWikiServices;
  6. use MediaWiki\Widget\SearchInputWidget;
  7. use SearchEngineConfig;
  8. use SpecialSearch;
  9. use Xml;
  10. class SearchFormWidget {
  11. /** @var SpecialSearch */
  12. protected $specialSearch;
  13. /** @var SearchEngineConfig */
  14. protected $searchConfig;
  15. /** @var array */
  16. protected $profiles;
  17. /**
  18. * @param SpecialSearch $specialSearch
  19. * @param SearchEngineConfig $searchConfig
  20. * @param array $profiles
  21. */
  22. public function __construct(
  23. SpecialSearch $specialSearch,
  24. SearchEngineConfig $searchConfig,
  25. array $profiles
  26. ) {
  27. $this->specialSearch = $specialSearch;
  28. $this->searchConfig = $searchConfig;
  29. $this->profiles = $profiles;
  30. }
  31. /**
  32. * @param string $profile The current search profile
  33. * @param string $term The current search term
  34. * @param int $numResults The number of results shown
  35. * @param int $totalResults The total estimated results found
  36. * @param int $offset Current offset in search results
  37. * @param bool $isPowerSearch Is the 'advanced' section open?
  38. * @param array $options Widget options
  39. * @return string HTML
  40. */
  41. public function render(
  42. $profile,
  43. $term,
  44. $numResults,
  45. $totalResults,
  46. $offset,
  47. $isPowerSearch,
  48. array $options = []
  49. ) {
  50. $user = $this->specialSearch->getUser();
  51. return '<div class="mw-search-form-wrapper">' .
  52. Xml::openElement(
  53. 'form',
  54. [
  55. 'id' => $isPowerSearch ? 'powersearch' : 'search',
  56. // T151903: default to POST in case JS is disabled
  57. 'method' => ( $isPowerSearch && $user->isLoggedIn() ) ? 'post' : 'get',
  58. 'action' => wfScript(),
  59. ]
  60. ) .
  61. '<div id="mw-search-top-table">' .
  62. $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset, $options ) .
  63. '</div>' .
  64. "<div class='mw-search-visualclear'></div>" .
  65. "<div class='mw-search-profile-tabs'>" .
  66. $this->profileTabsHtml( $profile, $term ) .
  67. "<div style='clear:both'></div>" .
  68. "</div>" .
  69. $this->optionsHtml( $term, $isPowerSearch, $profile ) .
  70. '</form>' .
  71. '</div>';
  72. }
  73. /**
  74. * @param string $profile The current search profile
  75. * @param string $term The current search term
  76. * @param int $numResults The number of results shown
  77. * @param int $totalResults The total estimated results found
  78. * @param int $offset Current offset in search results
  79. * @param array $options Widget options
  80. * @return string HTML
  81. */
  82. protected function shortDialogHtml(
  83. $profile,
  84. $term,
  85. $numResults,
  86. $totalResults,
  87. $offset,
  88. array $options = []
  89. ) {
  90. $html = '';
  91. $searchWidget = new SearchInputWidget( $options + [
  92. 'id' => 'searchText',
  93. 'name' => 'search',
  94. 'autofocus' => trim( $term ) === '',
  95. 'value' => $term,
  96. 'dataLocation' => 'content',
  97. 'infusable' => true,
  98. ] );
  99. $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [
  100. 'type' => 'submit',
  101. 'label' => $this->specialSearch->msg( 'searchbutton' )->text(),
  102. 'flags' => [ 'progressive', 'primary' ],
  103. ] ), [
  104. 'align' => 'top',
  105. ] );
  106. $html .= $layout;
  107. if ( $this->specialSearch->getPrefix() !== '' ) {
  108. $html .= Html::hidden( 'prefix', $this->specialSearch->getPrefix() );
  109. }
  110. if ( $totalResults > 0 && $offset < $totalResults ) {
  111. $html .= Xml::tags(
  112. 'div',
  113. [
  114. 'class' => 'results-info',
  115. 'data-mw-num-results-offset' => $offset,
  116. 'data-mw-num-results-total' => $totalResults
  117. ],
  118. $this->specialSearch->msg( 'search-showingresults' )
  119. ->numParams( $offset + 1, $offset + $numResults, $totalResults )
  120. ->numParams( $numResults )
  121. ->parse()
  122. );
  123. }
  124. $html .=
  125. Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) .
  126. Html::hidden( 'profile', $profile ) .
  127. Html::hidden( 'fulltext', '1' );
  128. return $html;
  129. }
  130. /**
  131. * Generates HTML for the list of available search profiles.
  132. *
  133. * @param string $profile The currently selected profile
  134. * @param string $term The user provided search terms
  135. * @return string HTML
  136. * @suppress PhanTypeArraySuspiciousNullable
  137. */
  138. protected function profileTabsHtml( $profile, $term ) {
  139. $bareterm = $this->startsWithImage( $term )
  140. ? substr( $term, strpos( $term, ':' ) + 1 )
  141. : $term;
  142. $lang = $this->specialSearch->getLanguage();
  143. $items = [];
  144. foreach ( $this->profiles as $id => $profileConfig ) {
  145. $profileConfig['parameters']['profile'] = $id;
  146. $tooltipParam = isset( $profileConfig['namespace-messages'] )
  147. ? $lang->commaList( $profileConfig['namespace-messages'] )
  148. : null;
  149. $items[] = Xml::tags(
  150. 'li',
  151. [ 'class' => $profile === $id ? 'current' : 'normal' ],
  152. $this->makeSearchLink(
  153. $bareterm,
  154. $this->specialSearch->msg( $profileConfig['message'] )->text(),
  155. $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(),
  156. $profileConfig['parameters']
  157. )
  158. );
  159. }
  160. return "<div class='search-types'>" .
  161. "<ul>" . implode( '', $items ) . "</ul>" .
  162. "</div>";
  163. }
  164. /**
  165. * Check if query starts with image: prefix
  166. *
  167. * @param string $term The string to check
  168. * @return bool
  169. */
  170. protected function startsWithImage( $term ) {
  171. $parts = explode( ':', $term );
  172. return count( $parts ) > 1
  173. ? MediaWikiServices::getInstance()->getContentLanguage()->getNsIndex( $parts[0] ) ===
  174. NS_FILE
  175. : false;
  176. }
  177. /**
  178. * Make a search link with some target namespaces
  179. *
  180. * @param string $term The term to search for
  181. * @param string $label Link's text
  182. * @param string $tooltip Link's tooltip
  183. * @param array $params Query string parameters
  184. * @return string HTML fragment
  185. */
  186. protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) {
  187. $params += [
  188. 'search' => $term,
  189. 'fulltext' => 1,
  190. ];
  191. return Xml::element(
  192. 'a',
  193. [
  194. 'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ),
  195. 'title' => $tooltip,
  196. ],
  197. $label
  198. );
  199. }
  200. /**
  201. * Generates HTML for advanced options available with the currently
  202. * selected search profile.
  203. *
  204. * @param string $term User provided search term
  205. * @param bool $isPowerSearch Is the advanced search profile enabled?
  206. * @param string $profile The current search profile
  207. * @return string HTML
  208. */
  209. protected function optionsHtml( $term, $isPowerSearch, $profile ) {
  210. $html = '';
  211. if ( $isPowerSearch ) {
  212. $html .= $this->powerSearchBox( $term, [] );
  213. } else {
  214. $form = '';
  215. Hooks::run( 'SpecialSearchProfileForm', [
  216. $this->specialSearch, &$form, $profile, $term, []
  217. ] );
  218. $html .= $form;
  219. }
  220. return $html;
  221. }
  222. /**
  223. * @param string $term The current search term
  224. * @param array $opts Additional key/value pairs that will be submitted
  225. * with the generated form.
  226. * @return string HTML
  227. */
  228. protected function powerSearchBox( $term, array $opts ) {
  229. $rows = [];
  230. $activeNamespaces = $this->specialSearch->getNamespaces();
  231. $langConverter = $this->specialSearch->getLanguage();
  232. foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
  233. $subject = MediaWikiServices::getInstance()->getNamespaceInfo()->
  234. getSubject( $namespace );
  235. if ( !isset( $rows[$subject] ) ) {
  236. $rows[$subject] = "";
  237. }
  238. $name = $langConverter->convertNamespace( $namespace );
  239. if ( $name === '' ) {
  240. $name = $this->specialSearch->msg( 'blanknamespace' )->text();
  241. }
  242. $rows[$subject] .=
  243. '<td>' .
  244. Xml::checkLabel(
  245. $name,
  246. "ns{$namespace}",
  247. "mw-search-ns{$namespace}",
  248. in_array( $namespace, $activeNamespaces )
  249. ) .
  250. '</td>';
  251. }
  252. // Lays out namespaces in multiple floating two-column tables so they'll
  253. // be arranged nicely while still accomodating diferent screen widths
  254. $tableRows = [];
  255. foreach ( $rows as $row ) {
  256. $tableRows[] = "<tr>{$row}</tr>";
  257. }
  258. $namespaceTables = [];
  259. foreach ( array_chunk( $tableRows, 4 ) as $chunk ) {
  260. $namespaceTables[] = implode( '', $chunk );
  261. }
  262. $showSections = [
  263. 'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>',
  264. ];
  265. Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, &$opts ] );
  266. $hidden = '';
  267. foreach ( $opts as $key => $value ) {
  268. $hidden .= Html::hidden( $key, $value );
  269. }
  270. $divider = "<div class='divider'></div>";
  271. // Stuff to feed SpecialSearch::saveNamespaces()
  272. $user = $this->specialSearch->getUser();
  273. $remember = '';
  274. if ( $user->isLoggedIn() ) {
  275. $remember = $divider . Xml::checkLabel(
  276. $this->specialSearch->msg( 'powersearch-remember' )->text(),
  277. 'nsRemember',
  278. 'mw-search-powersearch-remember',
  279. false,
  280. // The token goes here rather than in a hidden field so it
  281. // is only sent when necessary (not every form submission)
  282. [ 'value' => $user->getEditToken(
  283. 'searchnamespace',
  284. $this->specialSearch->getRequest()
  285. ) ]
  286. );
  287. }
  288. return "<fieldset id='mw-searchoptions'>" .
  289. "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' .
  290. "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' .
  291. // Handled by JavaScript if available
  292. '<div id="mw-search-togglebox">' .
  293. '<label>' . $this->specialSearch->msg( 'powersearch-togglelabel' )->escaped() . '</label>' .
  294. '<input type="button" id="mw-search-toggleall" value="' .
  295. $this->specialSearch->msg( 'powersearch-toggleall' )->escaped() . '"/>' .
  296. '<input type="button" id="mw-search-togglenone" value="' .
  297. $this->specialSearch->msg( 'powersearch-togglenone' )->escaped() . '"/>' .
  298. '</div>' .
  299. $divider .
  300. implode(
  301. $divider,
  302. $showSections
  303. ) .
  304. $hidden .
  305. $remember .
  306. "</fieldset>";
  307. }
  308. }