BaseTemplate.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. */
  20. use Wikimedia\WrappedString;
  21. use Wikimedia\WrappedStringList;
  22. /**
  23. * New base template for a skin's template extended from QuickTemplate
  24. * this class features helper methods that provide common ways of interacting
  25. * with the data stored in the QuickTemplate
  26. */
  27. abstract class BaseTemplate extends QuickTemplate {
  28. /**
  29. * Get a Message object with its context set
  30. *
  31. * @param string $name Message name
  32. * @param mixed ...$params Message params
  33. * @return Message
  34. */
  35. public function getMsg( $name, ...$params ) {
  36. return $this->getSkin()->msg( $name, ...$params );
  37. }
  38. function msg( $str ) {
  39. echo $this->getMsg( $str )->escaped();
  40. }
  41. /**
  42. * @deprecated since 1.33 Use ->msg() or ->getMsg() instead.
  43. */
  44. function msgWiki( $str ) {
  45. wfDeprecated( __METHOD__, '1.33' ); // Hard-deprecated in 1.34
  46. echo $this->getMsg( $str )->parseAsBlock();
  47. }
  48. /**
  49. * Create an array of common toolbox items from the data in the quicktemplate
  50. * stored by SkinTemplate.
  51. * The resulting array is built according to a format intended to be passed
  52. * through makeListItem to generate the html.
  53. * @return array
  54. */
  55. function getToolbox() {
  56. $toolbox = [];
  57. if ( isset( $this->data['nav_urls']['whatlinkshere'] )
  58. && $this->data['nav_urls']['whatlinkshere']
  59. ) {
  60. $toolbox['whatlinkshere'] = $this->data['nav_urls']['whatlinkshere'];
  61. $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
  62. }
  63. if ( isset( $this->data['nav_urls']['recentchangeslinked'] )
  64. && $this->data['nav_urls']['recentchangeslinked']
  65. ) {
  66. $toolbox['recentchangeslinked'] = $this->data['nav_urls']['recentchangeslinked'];
  67. $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
  68. $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
  69. $toolbox['recentchangeslinked']['rel'] = 'nofollow';
  70. }
  71. if ( isset( $this->data['feeds'] ) && $this->data['feeds'] ) {
  72. $toolbox['feeds']['id'] = 'feedlinks';
  73. $toolbox['feeds']['links'] = [];
  74. foreach ( $this->data['feeds'] as $key => $feed ) {
  75. $toolbox['feeds']['links'][$key] = $feed;
  76. $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
  77. $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
  78. $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
  79. $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
  80. }
  81. }
  82. foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
  83. 'userrights', 'upload', 'specialpages' ] as $special
  84. ) {
  85. if ( isset( $this->data['nav_urls'][$special] ) && $this->data['nav_urls'][$special] ) {
  86. $toolbox[$special] = $this->data['nav_urls'][$special];
  87. $toolbox[$special]['id'] = "t-$special";
  88. }
  89. }
  90. if ( isset( $this->data['nav_urls']['print'] ) && $this->data['nav_urls']['print'] ) {
  91. $toolbox['print'] = $this->data['nav_urls']['print'];
  92. $toolbox['print']['id'] = 't-print';
  93. $toolbox['print']['rel'] = 'alternate';
  94. $toolbox['print']['msg'] = 'printableversion';
  95. }
  96. if ( isset( $this->data['nav_urls']['permalink'] ) && $this->data['nav_urls']['permalink'] ) {
  97. $toolbox['permalink'] = $this->data['nav_urls']['permalink'];
  98. $toolbox['permalink']['id'] = 't-permalink';
  99. }
  100. if ( isset( $this->data['nav_urls']['info'] ) && $this->data['nav_urls']['info'] ) {
  101. $toolbox['info'] = $this->data['nav_urls']['info'];
  102. $toolbox['info']['id'] = 't-info';
  103. }
  104. // Avoid PHP 7.1 warning from passing $this by reference
  105. $template = $this;
  106. Hooks::run( 'BaseTemplateToolbox', [ &$template, &$toolbox ] );
  107. return $toolbox;
  108. }
  109. /**
  110. * Create an array of personal tools items from the data in the quicktemplate
  111. * stored by SkinTemplate.
  112. * The resulting array is built according to a format intended to be passed
  113. * through makeListItem to generate the html.
  114. * This is in reality the same list as already stored in personal_urls
  115. * however it is reformatted so that you can just pass the individual items
  116. * to makeListItem instead of hardcoding the element creation boilerplate.
  117. * @return array
  118. */
  119. function getPersonalTools() {
  120. $personal_tools = [];
  121. foreach ( $this->get( 'personal_urls' ) as $key => $plink ) {
  122. # The class on a personal_urls item is meant to go on the <a> instead
  123. # of the <li> so we have to use a single item "links" array instead
  124. # of using most of the personal_url's keys directly.
  125. $ptool = [
  126. 'links' => [
  127. [ 'single-id' => "pt-$key" ],
  128. ],
  129. 'id' => "pt-$key",
  130. ];
  131. if ( isset( $plink['active'] ) ) {
  132. $ptool['active'] = $plink['active'];
  133. }
  134. foreach ( [ 'href', 'class', 'text', 'dir', 'data', 'exists' ] as $k ) {
  135. if ( isset( $plink[$k] ) ) {
  136. $ptool['links'][0][$k] = $plink[$k];
  137. }
  138. }
  139. $personal_tools[$key] = $ptool;
  140. }
  141. return $personal_tools;
  142. }
  143. function getSidebar( $options = [] ) {
  144. // Force the rendering of the following portals
  145. $sidebar = $this->data['sidebar'];
  146. if ( !isset( $sidebar['SEARCH'] ) ) {
  147. $sidebar['SEARCH'] = true;
  148. }
  149. if ( !isset( $sidebar['TOOLBOX'] ) ) {
  150. $sidebar['TOOLBOX'] = true;
  151. }
  152. if ( !isset( $sidebar['LANGUAGES'] ) ) {
  153. $sidebar['LANGUAGES'] = true;
  154. }
  155. if ( !isset( $options['search'] ) || $options['search'] !== true ) {
  156. unset( $sidebar['SEARCH'] );
  157. }
  158. if ( isset( $options['toolbox'] ) && $options['toolbox'] === false ) {
  159. unset( $sidebar['TOOLBOX'] );
  160. }
  161. if ( isset( $options['languages'] ) && $options['languages'] === false ) {
  162. unset( $sidebar['LANGUAGES'] );
  163. }
  164. $boxes = [];
  165. foreach ( $sidebar as $boxName => $content ) {
  166. if ( $content === false ) {
  167. continue;
  168. }
  169. switch ( $boxName ) {
  170. case 'SEARCH':
  171. // Search is a special case, skins should custom implement this
  172. $boxes[$boxName] = [
  173. 'id' => 'p-search',
  174. 'header' => $this->getMsg( 'search' )->text(),
  175. 'generated' => false,
  176. 'content' => true,
  177. ];
  178. break;
  179. case 'TOOLBOX':
  180. $msgObj = $this->getMsg( 'toolbox' );
  181. $boxes[$boxName] = [
  182. 'id' => 'p-tb',
  183. 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
  184. 'generated' => false,
  185. 'content' => $this->getToolbox(),
  186. ];
  187. break;
  188. case 'LANGUAGES':
  189. if ( $this->data['language_urls'] !== false ) {
  190. $msgObj = $this->getMsg( 'otherlanguages' );
  191. $boxes[$boxName] = [
  192. 'id' => 'p-lang',
  193. 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
  194. 'generated' => false,
  195. 'content' => $this->data['language_urls'] ?: [],
  196. ];
  197. }
  198. break;
  199. default:
  200. $msgObj = $this->getMsg( $boxName );
  201. $boxes[$boxName] = [
  202. 'id' => "p-$boxName",
  203. 'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
  204. 'generated' => true,
  205. 'content' => $content,
  206. ];
  207. break;
  208. }
  209. }
  210. // HACK: Compatibility with extensions still using SkinTemplateToolboxEnd
  211. $hookContents = null;
  212. if ( isset( $boxes['TOOLBOX'] ) ) {
  213. ob_start();
  214. // We pass an extra 'true' at the end so extensions using BaseTemplateToolbox
  215. // can abort and avoid outputting double toolbox links
  216. // Avoid PHP 7.1 warning from passing $this by reference
  217. $template = $this;
  218. Hooks::run( 'SkinTemplateToolboxEnd', [ &$template, true ] );
  219. $hookContents = ob_get_contents();
  220. ob_end_clean();
  221. if ( !trim( $hookContents ) ) {
  222. $hookContents = null;
  223. }
  224. }
  225. // END hack
  226. if ( isset( $options['htmlOnly'] ) && $options['htmlOnly'] === true ) {
  227. foreach ( $boxes as $boxName => $box ) {
  228. if ( is_array( $box['content'] ) ) {
  229. $content = '<ul>';
  230. foreach ( $box['content'] as $key => $val ) {
  231. $content .= "\n " . $this->makeListItem( $key, $val );
  232. }
  233. // HACK, shove the toolbox end onto the toolbox if we're rendering itself
  234. if ( $hookContents ) {
  235. $content .= "\n $hookContents";
  236. }
  237. // END hack
  238. $content .= "\n</ul>\n";
  239. $boxes[$boxName]['content'] = $content;
  240. }
  241. }
  242. } elseif ( $hookContents ) {
  243. $boxes['TOOLBOXEND'] = [
  244. 'id' => 'p-toolboxend',
  245. 'header' => $boxes['TOOLBOX']['header'],
  246. 'generated' => false,
  247. 'content' => "<ul>{$hookContents}</ul>",
  248. ];
  249. // HACK: Make sure that TOOLBOXEND is sorted next to TOOLBOX
  250. $boxes2 = [];
  251. foreach ( $boxes as $key => $box ) {
  252. if ( $key === 'TOOLBOXEND' ) {
  253. continue;
  254. }
  255. $boxes2[$key] = $box;
  256. if ( $key === 'TOOLBOX' ) {
  257. $boxes2['TOOLBOXEND'] = $boxes['TOOLBOXEND'];
  258. }
  259. }
  260. $boxes = $boxes2;
  261. // END hack
  262. }
  263. return $boxes;
  264. }
  265. /**
  266. * @param string $name
  267. */
  268. protected function renderAfterPortlet( $name ) {
  269. echo $this->getAfterPortlet( $name );
  270. }
  271. /**
  272. * Allows extensions to hook into known portlets and add stuff to them
  273. *
  274. * @param string $name
  275. *
  276. * @return string html
  277. * @since 1.29
  278. */
  279. protected function getAfterPortlet( $name ) {
  280. $html = '';
  281. $content = '';
  282. Hooks::run( 'BaseTemplateAfterPortlet', [ $this, $name, &$content ] );
  283. if ( $content !== '' ) {
  284. $html = Html::rawElement(
  285. 'div',
  286. [ 'class' => [ 'after-portlet', 'after-portlet-' . $name ] ],
  287. $content
  288. );
  289. }
  290. return $html;
  291. }
  292. /**
  293. * Makes a link, usually used by makeListItem to generate a link for an item
  294. * in a list used in navigation lists, portlets, portals, sidebars, etc...
  295. *
  296. * @param string $key Usually a key from the list you are generating this
  297. * link from.
  298. * @param array $item Contains some of a specific set of keys.
  299. *
  300. * The text of the link will be generated either from the contents of the
  301. * "text" key in the $item array, if a "msg" key is present a message by
  302. * that name will be used, and if neither of those are set the $key will be
  303. * used as a message name.
  304. *
  305. * If a "href" key is not present makeLink will just output htmlescaped text.
  306. * The "href", "id", "class", "rel", and "type" keys are used as attributes
  307. * for the link if present.
  308. *
  309. * If an "id" or "single-id" (if you don't want the actual id to be output
  310. * on the link) is present it will be used to generate a tooltip and
  311. * accesskey for the link.
  312. *
  313. * The keys "context" and "primary" are ignored; these keys are used
  314. * internally by skins and are not supposed to be included in the HTML
  315. * output.
  316. *
  317. * If you don't want an accesskey, set $item['tooltiponly'] = true;
  318. *
  319. * If a "data" key is present, it must be an array, where the keys represent
  320. * the data-xxx properties with their provided values. For example,
  321. * $item['data'] = [
  322. * 'foo' => 1,
  323. * 'bar' => 'baz',
  324. * ];
  325. * will render as element properties:
  326. * data-foo='1' data-bar='baz'
  327. *
  328. * @param array $options Can be used to affect the output of a link.
  329. * Possible options are:
  330. * - 'text-wrapper' key to specify a list of elements to wrap the text of
  331. * a link in. This should be an array of arrays containing a 'tag' and
  332. * optionally an 'attributes' key. If you only have one element you don't
  333. * need to wrap it in another array. eg: To use <a><span>...</span></a>
  334. * in all links use [ 'text-wrapper' => [ 'tag' => 'span' ] ]
  335. * for your options.
  336. * - 'link-class' key can be used to specify additional classes to apply
  337. * to all links.
  338. * - 'link-fallback' can be used to specify a tag to use instead of "<a>"
  339. * if there is no link. eg: If you specify 'link-fallback' => 'span' than
  340. * any non-link will output a "<span>" instead of just text.
  341. *
  342. * @return string
  343. */
  344. function makeLink( $key, $item, $options = [] ) {
  345. $text = $item['text'] ?? wfMessage( $item['msg'] ?? $key )->text();
  346. $html = htmlspecialchars( $text );
  347. if ( isset( $options['text-wrapper'] ) ) {
  348. $wrapper = $options['text-wrapper'];
  349. if ( isset( $wrapper['tag'] ) ) {
  350. $wrapper = [ $wrapper ];
  351. }
  352. while ( count( $wrapper ) > 0 ) {
  353. $element = array_pop( $wrapper );
  354. $html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
  355. }
  356. }
  357. if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
  358. $attrs = $item;
  359. foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
  360. 'tooltip-params', 'exists' ] as $k ) {
  361. unset( $attrs[$k] );
  362. }
  363. if ( isset( $attrs['data'] ) ) {
  364. foreach ( $attrs['data'] as $key => $value ) {
  365. $attrs[ 'data-' . $key ] = $value;
  366. }
  367. unset( $attrs[ 'data' ] );
  368. }
  369. if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
  370. $item['single-id'] = $item['id'];
  371. }
  372. $tooltipParams = [];
  373. if ( isset( $item['tooltip-params'] ) ) {
  374. $tooltipParams = $item['tooltip-params'];
  375. }
  376. if ( isset( $item['single-id'] ) ) {
  377. $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
  378. if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
  379. $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
  380. if ( $title !== false ) {
  381. $attrs['title'] = $title;
  382. }
  383. } else {
  384. $tip = Linker::tooltipAndAccesskeyAttribs(
  385. $item['single-id'],
  386. $tooltipParams,
  387. $tooltipOption
  388. );
  389. if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
  390. $attrs['title'] = $tip['title'];
  391. }
  392. if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
  393. $attrs['accesskey'] = $tip['accesskey'];
  394. }
  395. }
  396. }
  397. if ( isset( $options['link-class'] ) ) {
  398. if ( isset( $attrs['class'] ) ) {
  399. $attrs['class'] .= " {$options['link-class']}";
  400. } else {
  401. $attrs['class'] = $options['link-class'];
  402. }
  403. }
  404. $html = Html::rawElement( isset( $attrs['href'] )
  405. ? 'a'
  406. : $options['link-fallback'], $attrs, $html );
  407. }
  408. return $html;
  409. }
  410. /**
  411. * Generates a list item for a navigation, portlet, portal, sidebar... list
  412. *
  413. * @param string $key Usually a key from the list you are generating this link from.
  414. * @param array $item Array of list item data containing some of a specific set of keys.
  415. * The "id", "class" and "itemtitle" keys will be used as attributes for the list item,
  416. * if "active" contains a value of true a "active" class will also be appended to class.
  417. * @phan-param array{id?:string,class?:string,itemtitle?:string,active?:bool} $item
  418. *
  419. * @param array $options
  420. * @phan-param array{tag?:string} $options
  421. *
  422. * If you want something other than a "<li>" you can pass a tag name such as
  423. * "tag" => "span" in the $options array to change the tag used.
  424. * link/content data for the list item may come in one of two forms
  425. * A "links" key may be used, in which case it should contain an array with
  426. * a list of links to include inside the list item, see makeLink for the
  427. * format of individual links array items.
  428. *
  429. * Otherwise the relevant keys from the list item $item array will be passed
  430. * to makeLink instead. Note however that "id" and "class" are used by the
  431. * list item directly so they will not be passed to makeLink
  432. * (however the link will still support a tooltip and accesskey from it)
  433. * If you need an id or class on a single link you should include a "links"
  434. * array with just one link item inside of it. You can also set "link-class" in
  435. * $item to set a class on the link itself. If you want to add a title
  436. * to the list item itself, you can set "itemtitle" to the value.
  437. * $options is also passed on to makeLink calls
  438. *
  439. * @return string
  440. */
  441. function makeListItem( $key, $item, $options = [] ) {
  442. // In case this is still set from SkinTemplate, we don't want it to appear in
  443. // the HTML output (normally removed in SkinTemplate::buildContentActionUrls())
  444. unset( $item['redundant'] );
  445. if ( isset( $item['links'] ) ) {
  446. $links = [];
  447. foreach ( $item['links'] as $linkKey => $link ) {
  448. $links[] = $this->makeLink( $linkKey, $link, $options );
  449. }
  450. $html = implode( ' ', $links );
  451. } else {
  452. $link = $item;
  453. // These keys are used by makeListItem and shouldn't be passed on to the link
  454. foreach ( [ 'id', 'class', 'active', 'tag', 'itemtitle' ] as $k ) {
  455. unset( $link[$k] );
  456. }
  457. if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
  458. // The id goes on the <li> not on the <a> for single links
  459. // but makeSidebarLink still needs to know what id to use when
  460. // generating tooltips and accesskeys.
  461. $link['single-id'] = $item['id'];
  462. }
  463. if ( isset( $link['link-class'] ) ) {
  464. // link-class should be set on the <a> itself,
  465. // so pass it in as 'class'
  466. $link['class'] = $link['link-class'];
  467. unset( $link['link-class'] );
  468. }
  469. $html = $this->makeLink( $key, $link, $options );
  470. }
  471. $attrs = [];
  472. foreach ( [ 'id', 'class' ] as $attr ) {
  473. if ( isset( $item[$attr] ) ) {
  474. $attrs[$attr] = $item[$attr];
  475. }
  476. }
  477. if ( isset( $item['active'] ) && $item['active'] ) {
  478. if ( !isset( $attrs['class'] ) ) {
  479. $attrs['class'] = '';
  480. }
  481. $attrs['class'] .= ' active';
  482. $attrs['class'] = trim( $attrs['class'] );
  483. }
  484. if ( isset( $item['itemtitle'] ) ) {
  485. $attrs['title'] = $item['itemtitle'];
  486. }
  487. return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
  488. }
  489. function makeSearchInput( $attrs = [] ) {
  490. $realAttrs = [
  491. 'type' => 'search',
  492. 'name' => 'search',
  493. 'placeholder' => wfMessage( 'searchsuggest-search' )->text(),
  494. ];
  495. $realAttrs = array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
  496. return Html::element( 'input', $realAttrs );
  497. }
  498. function makeSearchButton( $mode, $attrs = [] ) {
  499. switch ( $mode ) {
  500. case 'go':
  501. case 'fulltext':
  502. $realAttrs = [
  503. 'type' => 'submit',
  504. 'name' => $mode,
  505. 'value' => wfMessage( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
  506. ];
  507. $realAttrs = array_merge(
  508. $realAttrs,
  509. Linker::tooltipAndAccesskeyAttribs( "search-$mode" ),
  510. $attrs
  511. );
  512. return Html::element( 'input', $realAttrs );
  513. case 'image':
  514. $buttonAttrs = [
  515. 'type' => 'submit',
  516. 'name' => 'button',
  517. ];
  518. $buttonAttrs = array_merge(
  519. $buttonAttrs,
  520. Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ),
  521. $attrs
  522. );
  523. unset( $buttonAttrs['src'] );
  524. unset( $buttonAttrs['alt'] );
  525. unset( $buttonAttrs['width'] );
  526. unset( $buttonAttrs['height'] );
  527. $imgAttrs = [
  528. 'src' => $attrs['src'],
  529. 'alt' => $attrs['alt'] ?? wfMessage( 'searchbutton' )->text(),
  530. 'width' => $attrs['width'] ?? null,
  531. 'height' => $attrs['height'] ?? null,
  532. ];
  533. return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
  534. default:
  535. throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' );
  536. }
  537. }
  538. /**
  539. * Returns an array of footerlinks trimmed down to only those footer links that
  540. * are valid.
  541. * If you pass "flat" as an option then the returned array will be a flat array
  542. * of footer icons instead of a key/value array of footerlinks arrays broken
  543. * up into categories.
  544. * @param string|null $option
  545. * @return array|mixed
  546. */
  547. function getFooterLinks( $option = null ) {
  548. $footerlinks = $this->get( 'footerlinks' );
  549. // Reduce footer links down to only those which are being used
  550. $validFooterLinks = [];
  551. foreach ( $footerlinks as $category => $links ) {
  552. $validFooterLinks[$category] = [];
  553. foreach ( $links as $link ) {
  554. if ( isset( $this->data[$link] ) && $this->data[$link] ) {
  555. $validFooterLinks[$category][] = $link;
  556. }
  557. }
  558. if ( count( $validFooterLinks[$category] ) <= 0 ) {
  559. unset( $validFooterLinks[$category] );
  560. }
  561. }
  562. if ( $option == 'flat' ) {
  563. // fold footerlinks into a single array using a bit of trickery
  564. $validFooterLinks = array_merge( ...array_values( $validFooterLinks ) );
  565. }
  566. return $validFooterLinks;
  567. }
  568. /**
  569. * Returns an array of footer icons filtered down by options relevant to how
  570. * the skin wishes to display them.
  571. * If you pass "icononly" as the option all footer icons which do not have an
  572. * image icon set will be filtered out.
  573. * If you pass "nocopyright" then MediaWiki's copyright icon will not be included
  574. * in the list of footer icons. This is mostly useful for skins which only
  575. * display the text from footericons instead of the images and don't want a
  576. * duplicate copyright statement because footerlinks already rendered one.
  577. * @param string|null $option
  578. * @return array
  579. */
  580. function getFooterIcons( $option = null ) {
  581. // Generate additional footer icons
  582. $footericons = $this->get( 'footericons' );
  583. if ( $option == 'icononly' ) {
  584. // Unset any icons which don't have an image
  585. foreach ( $footericons as &$footerIconsBlock ) {
  586. foreach ( $footerIconsBlock as $footerIconKey => $footerIcon ) {
  587. if ( !is_string( $footerIcon ) && !isset( $footerIcon['src'] ) ) {
  588. unset( $footerIconsBlock[$footerIconKey] );
  589. }
  590. }
  591. }
  592. // Redo removal of any empty blocks
  593. foreach ( $footericons as $footerIconsKey => &$footerIconsBlock ) {
  594. if ( count( $footerIconsBlock ) <= 0 ) {
  595. unset( $footericons[$footerIconsKey] );
  596. }
  597. }
  598. } elseif ( $option == 'nocopyright' ) {
  599. unset( $footericons['copyright']['copyright'] );
  600. if ( count( $footericons['copyright'] ) <= 0 ) {
  601. unset( $footericons['copyright'] );
  602. }
  603. }
  604. return $footericons;
  605. }
  606. /**
  607. * Renderer for getFooterIcons and getFooterLinks
  608. *
  609. * @param string $iconStyle $option for getFooterIcons: "icononly", "nocopyright"
  610. * @param string $linkStyle $option for getFooterLinks: "flat"
  611. *
  612. * @return string html
  613. * @since 1.29
  614. */
  615. protected function getFooter( $iconStyle = 'icononly', $linkStyle = 'flat' ) {
  616. $validFooterIcons = $this->getFooterIcons( $iconStyle );
  617. $validFooterLinks = $this->getFooterLinks( $linkStyle );
  618. $html = '';
  619. if ( count( $validFooterIcons ) + count( $validFooterLinks ) > 0 ) {
  620. $html .= Html::openElement( 'div', [
  621. 'id' => 'footer-bottom',
  622. 'role' => 'contentinfo',
  623. 'lang' => $this->get( 'userlang' ),
  624. 'dir' => $this->get( 'dir' )
  625. ] );
  626. $footerEnd = Html::closeElement( 'div' );
  627. } else {
  628. $footerEnd = '';
  629. }
  630. foreach ( $validFooterIcons as $blockName => $footerIcons ) {
  631. $html .= Html::openElement( 'div', [
  632. 'id' => Sanitizer::escapeIdForAttribute( "f-{$blockName}ico" ),
  633. 'class' => 'footer-icons'
  634. ] );
  635. foreach ( $footerIcons as $icon ) {
  636. $html .= $this->getSkin()->makeFooterIcon( $icon );
  637. }
  638. $html .= Html::closeElement( 'div' );
  639. }
  640. if ( count( $validFooterLinks ) > 0 ) {
  641. $html .= Html::openElement( 'ul', [ 'id' => 'f-list', 'class' => 'footer-places' ] );
  642. foreach ( $validFooterLinks as $aLink ) {
  643. $html .= Html::rawElement(
  644. 'li',
  645. [ 'id' => Sanitizer::escapeIdForAttribute( $aLink ) ],
  646. $this->get( $aLink )
  647. );
  648. }
  649. $html .= Html::closeElement( 'ul' );
  650. }
  651. $html .= $this->getClear() . $footerEnd;
  652. return $html;
  653. }
  654. /**
  655. * Get a div with the core visualClear class, for clearing floats
  656. *
  657. * @return string html
  658. * @since 1.29
  659. */
  660. protected function getClear() {
  661. return Html::element( 'div', [ 'class' => 'visualClear' ] );
  662. }
  663. /**
  664. * Get the suggested HTML for page status indicators: icons (or short text snippets) usually
  665. * displayed in the top-right corner of the page, outside of the main content.
  666. *
  667. * Your skin may implement this differently, for example by handling some indicator names
  668. * specially with a different UI. However, it is recommended to use a `<div class="mw-indicator"
  669. * id="mw-indicator-<id>" />` as a wrapper element for each indicator, for better compatibility
  670. * with extensions and user scripts.
  671. *
  672. * The raw data is available in `$this->data['indicators']` as an associative array (keys:
  673. * identifiers, values: contents) internally ordered by keys.
  674. *
  675. * @return string HTML
  676. * @since 1.25
  677. */
  678. public function getIndicators() {
  679. $out = "<div class=\"mw-indicators mw-body-content\">\n";
  680. foreach ( $this->data['indicators'] as $id => $content ) {
  681. $out .= Html::rawElement(
  682. 'div',
  683. [
  684. 'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
  685. 'class' => 'mw-indicator',
  686. ],
  687. $content
  688. ) . "\n";
  689. }
  690. $out .= "</div>\n";
  691. return $out;
  692. }
  693. /**
  694. * Output getTrail
  695. */
  696. function printTrail() {
  697. echo $this->getTrail();
  698. }
  699. /**
  700. * Get the basic end-page trail including bottomscripts, reporttime, and
  701. * debug stuff. This should be called right before outputting the closing
  702. * body and html tags.
  703. *
  704. * @return string|WrappedStringList HTML
  705. * @since 1.29
  706. */
  707. public function getTrail() {
  708. return WrappedString::join( "\n", [
  709. // @phan-suppress-next-line PhanTypeMismatchArgument
  710. MWDebug::getDebugHTML( $this->getSkin()->getContext() ),
  711. $this->get( 'bottomscripts' ),
  712. $this->get( 'reporttime' )
  713. ] );
  714. }
  715. }