MediaWiki  master
BaseTemplate.php
Go to the documentation of this file.
1 <?php
23 
29 abstract class BaseTemplate extends QuickTemplate {
30 
38  public function getMsg( $name, ...$params ) {
39  return $this->getSkin()->msg( $name, ...$params );
40  }
41 
42  function msg( $str ) {
43  echo $this->getMsg( $str )->escaped();
44  }
45 
49  function msgWiki( $str ) {
50  wfDeprecated( __METHOD__, '1.33' ); // Hard-deprecated in 1.34
51  echo $this->getMsg( $str )->parseAsBlock();
52  }
53 
61  function getToolbox() {
62  $toolbox = [];
63  if ( isset( $this->data['nav_urls']['whatlinkshere'] )
64  && $this->data['nav_urls']['whatlinkshere']
65  ) {
66  $toolbox['whatlinkshere'] = $this->data['nav_urls']['whatlinkshere'];
67  $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
68  }
69  if ( isset( $this->data['nav_urls']['recentchangeslinked'] )
70  && $this->data['nav_urls']['recentchangeslinked']
71  ) {
72  $toolbox['recentchangeslinked'] = $this->data['nav_urls']['recentchangeslinked'];
73  $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
74  $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
75  $toolbox['recentchangeslinked']['rel'] = 'nofollow';
76  }
77  if ( isset( $this->data['feeds'] ) && $this->data['feeds'] ) {
78  $toolbox['feeds']['id'] = 'feedlinks';
79  $toolbox['feeds']['links'] = [];
80  foreach ( $this->data['feeds'] as $key => $feed ) {
81  $toolbox['feeds']['links'][$key] = $feed;
82  $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
83  $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
84  $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
85  $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
86  }
87  }
88  foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
89  'userrights', 'upload', 'specialpages' ] as $special
90  ) {
91  if ( isset( $this->data['nav_urls'][$special] ) && $this->data['nav_urls'][$special] ) {
92  $toolbox[$special] = $this->data['nav_urls'][$special];
93  $toolbox[$special]['id'] = "t-$special";
94  }
95  }
96  if ( isset( $this->data['nav_urls']['print'] ) && $this->data['nav_urls']['print'] ) {
97  $toolbox['print'] = $this->data['nav_urls']['print'];
98  $toolbox['print']['id'] = 't-print';
99  $toolbox['print']['rel'] = 'alternate';
100  $toolbox['print']['msg'] = 'printableversion';
101  }
102  if ( isset( $this->data['nav_urls']['permalink'] ) && $this->data['nav_urls']['permalink'] ) {
103  $toolbox['permalink'] = $this->data['nav_urls']['permalink'];
104  $toolbox['permalink']['id'] = 't-permalink';
105  }
106  if ( isset( $this->data['nav_urls']['info'] ) && $this->data['nav_urls']['info'] ) {
107  $toolbox['info'] = $this->data['nav_urls']['info'];
108  $toolbox['info']['id'] = 't-info';
109  }
110 
111  // Avoid PHP 7.1 warning from passing $this by reference
112  $template = $this;
113  Hooks::run( 'BaseTemplateToolbox', [ &$template, &$toolbox ] );
114  return $toolbox;
115  }
116 
127  function getPersonalTools() {
128  $personal_tools = [];
129  foreach ( $this->get( 'personal_urls' ) as $key => $plink ) {
130  # The class on a personal_urls item is meant to go on the <a> instead
131  # of the <li> so we have to use a single item "links" array instead
132  # of using most of the personal_url's keys directly.
133  $ptool = [
134  'links' => [
135  [ 'single-id' => "pt-$key" ],
136  ],
137  'id' => "pt-$key",
138  ];
139  if ( isset( $plink['active'] ) ) {
140  $ptool['active'] = $plink['active'];
141  }
142  foreach ( [ 'href', 'class', 'text', 'dir', 'data', 'exists' ] as $k ) {
143  if ( isset( $plink[$k] ) ) {
144  $ptool['links'][0][$k] = $plink[$k];
145  }
146  }
147  $personal_tools[$key] = $ptool;
148  }
149  return $personal_tools;
150  }
151 
152  function getSidebar( $options = [] ) {
153  // Force the rendering of the following portals
154  $sidebar = $this->data['sidebar'];
155  if ( !isset( $sidebar['SEARCH'] ) ) {
156  $sidebar['SEARCH'] = true;
157  }
158  if ( !isset( $sidebar['TOOLBOX'] ) ) {
159  $sidebar['TOOLBOX'] = true;
160  }
161  if ( !isset( $sidebar['LANGUAGES'] ) ) {
162  $sidebar['LANGUAGES'] = true;
163  }
164 
165  if ( !isset( $options['search'] ) || $options['search'] !== true ) {
166  unset( $sidebar['SEARCH'] );
167  }
168  if ( isset( $options['toolbox'] ) && $options['toolbox'] === false ) {
169  unset( $sidebar['TOOLBOX'] );
170  }
171  if ( isset( $options['languages'] ) && $options['languages'] === false ) {
172  unset( $sidebar['LANGUAGES'] );
173  }
174 
175  $boxes = [];
176  foreach ( $sidebar as $boxName => $content ) {
177  if ( $content === false ) {
178  continue;
179  }
180  switch ( $boxName ) {
181  case 'SEARCH':
182  // Search is a special case, skins should custom implement this
183  $boxes[$boxName] = [
184  'id' => 'p-search',
185  'header' => $this->getMsg( 'search' )->text(),
186  'generated' => false,
187  'content' => true,
188  ];
189  break;
190  case 'TOOLBOX':
191  $msgObj = $this->getMsg( 'toolbox' );
192  $boxes[$boxName] = [
193  'id' => 'p-tb',
194  'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
195  'generated' => false,
196  'content' => $this->getToolbox(),
197  ];
198  break;
199  case 'LANGUAGES':
200  if ( $this->data['language_urls'] !== false ) {
201  $msgObj = $this->getMsg( 'otherlanguages' );
202  $boxes[$boxName] = [
203  'id' => 'p-lang',
204  'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
205  'generated' => false,
206  'content' => $this->data['language_urls'] ?: [],
207  ];
208  }
209  break;
210  default:
211  $msgObj = $this->getMsg( $boxName );
212  $boxes[$boxName] = [
213  'id' => "p-$boxName",
214  'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
215  'generated' => true,
216  'content' => $content,
217  ];
218  break;
219  }
220  }
221 
222  // HACK: Compatibility with extensions still using SkinTemplateToolboxEnd
223  $hookContents = null;
224  if ( isset( $boxes['TOOLBOX'] ) ) {
225  ob_start();
226  // We pass an extra 'true' at the end so extensions using BaseTemplateToolbox
227  // can abort and avoid outputting double toolbox links
228  // Avoid PHP 7.1 warning from passing $this by reference
229  $template = $this;
230  Hooks::run( 'SkinTemplateToolboxEnd', [ &$template, true ] );
231  $hookContents = ob_get_contents();
232  ob_end_clean();
233  if ( !trim( $hookContents ) ) {
234  $hookContents = null;
235  }
236  }
237  // END hack
238 
239  if ( isset( $options['htmlOnly'] ) && $options['htmlOnly'] === true ) {
240  foreach ( $boxes as $boxName => $box ) {
241  if ( is_array( $box['content'] ) ) {
242  $content = '<ul>';
243  foreach ( $box['content'] as $key => $val ) {
244  $content .= "\n " . $this->makeListItem( $key, $val );
245  }
246  // HACK, shove the toolbox end onto the toolbox if we're rendering itself
247  if ( $hookContents ) {
248  $content .= "\n $hookContents";
249  }
250  // END hack
251  $content .= "\n</ul>\n";
252  $boxes[$boxName]['content'] = $content;
253  }
254  }
255  } elseif ( $hookContents ) {
256  $boxes['TOOLBOXEND'] = [
257  'id' => 'p-toolboxend',
258  'header' => $boxes['TOOLBOX']['header'],
259  'generated' => false,
260  'content' => "<ul>{$hookContents}</ul>",
261  ];
262  // HACK: Make sure that TOOLBOXEND is sorted next to TOOLBOX
263  $boxes2 = [];
264  foreach ( $boxes as $key => $box ) {
265  if ( $key === 'TOOLBOXEND' ) {
266  continue;
267  }
268  $boxes2[$key] = $box;
269  if ( $key === 'TOOLBOX' ) {
270  $boxes2['TOOLBOXEND'] = $boxes['TOOLBOXEND'];
271  }
272  }
273  $boxes = $boxes2;
274  // END hack
275  }
276 
277  return $boxes;
278  }
279 
283  protected function renderAfterPortlet( $name ) {
284  echo $this->getAfterPortlet( $name );
285  }
286 
295  protected function getAfterPortlet( $name ) {
296  $html = '';
297  $content = '';
298  Hooks::run( 'BaseTemplateAfterPortlet', [ $this, $name, &$content ] );
299 
300  if ( $content !== '' ) {
301  $html = Html::rawElement(
302  'div',
303  [ 'class' => [ 'after-portlet', 'after-portlet-' . $name ] ],
304  $content
305  );
306  }
307 
308  return $html;
309  }
310 
363  function makeLink( $key, $item, $options = [] ) {
364  $text = $item['text'] ?? wfMessage( $item['msg'] ?? $key )->text();
365 
366  $html = htmlspecialchars( $text );
367 
368  if ( isset( $options['text-wrapper'] ) ) {
369  $wrapper = $options['text-wrapper'];
370  if ( isset( $wrapper['tag'] ) ) {
371  $wrapper = [ $wrapper ];
372  }
373  while ( count( $wrapper ) > 0 ) {
374  $element = array_pop( $wrapper );
375  $html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
376  }
377  }
378 
379  if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
380  $attrs = $item;
381  foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
382  'tooltip-params', 'exists' ] as $k ) {
383  unset( $attrs[$k] );
384  }
385 
386  if ( isset( $attrs['data'] ) ) {
387  foreach ( $attrs['data'] as $key => $value ) {
388  $attrs[ 'data-' . $key ] = $value;
389  }
390  unset( $attrs[ 'data' ] );
391  }
392 
393  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
394  $item['single-id'] = $item['id'];
395  }
396 
397  $tooltipParams = [];
398  if ( isset( $item['tooltip-params'] ) ) {
399  $tooltipParams = $item['tooltip-params'];
400  }
401 
402  if ( isset( $item['single-id'] ) ) {
403  $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
404 
405  if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
406  $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
407  if ( $title !== false ) {
408  $attrs['title'] = $title;
409  }
410  } else {
412  $item['single-id'],
413  $tooltipParams,
414  $tooltipOption
415  );
416  if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
417  $attrs['title'] = $tip['title'];
418  }
419  if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
420  $attrs['accesskey'] = $tip['accesskey'];
421  }
422  }
423  }
424  if ( isset( $options['link-class'] ) ) {
425  if ( isset( $attrs['class'] ) ) {
426  $attrs['class'] .= " {$options['link-class']}";
427  } else {
428  $attrs['class'] = $options['link-class'];
429  }
430  }
431  $html = Html::rawElement( isset( $attrs['href'] )
432  ? 'a'
433  : $options['link-fallback'], $attrs, $html );
434  }
435 
436  return $html;
437  }
438 
470  function makeListItem( $key, $item, $options = [] ) {
471  // In case this is still set from SkinTemplate, we don't want it to appear in
472  // the HTML output (normally removed in SkinTemplate::buildContentActionUrls())
473  unset( $item['redundant'] );
474 
475  if ( isset( $item['links'] ) ) {
476  $links = [];
477  foreach ( $item['links'] as $linkKey => $link ) {
478  $links[] = $this->makeLink( $linkKey, $link, $options );
479  }
480  $html = implode( ' ', $links );
481  } else {
482  $link = $item;
483  // These keys are used by makeListItem and shouldn't be passed on to the link
484  foreach ( [ 'id', 'class', 'active', 'tag', 'itemtitle' ] as $k ) {
485  unset( $link[$k] );
486  }
487  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
488  // The id goes on the <li> not on the <a> for single links
489  // but makeSidebarLink still needs to know what id to use when
490  // generating tooltips and accesskeys.
491  $link['single-id'] = $item['id'];
492  }
493  if ( isset( $link['link-class'] ) ) {
494  // link-class should be set on the <a> itself,
495  // so pass it in as 'class'
496  $link['class'] = $link['link-class'];
497  unset( $link['link-class'] );
498  }
499  $html = $this->makeLink( $key, $link, $options );
500  }
501 
502  $attrs = [];
503  foreach ( [ 'id', 'class' ] as $attr ) {
504  if ( isset( $item[$attr] ) ) {
505  $attrs[$attr] = $item[$attr];
506  }
507  }
508  if ( isset( $item['active'] ) && $item['active'] ) {
509  if ( !isset( $attrs['class'] ) ) {
510  $attrs['class'] = '';
511  }
512  $attrs['class'] .= ' active';
513  $attrs['class'] = trim( $attrs['class'] );
514  }
515  if ( isset( $item['itemtitle'] ) ) {
516  $attrs['title'] = $item['itemtitle'];
517  }
518  return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
519  }
520 
521  function makeSearchInput( $attrs = [] ) {
522  $realAttrs = [
523  'type' => 'search',
524  'name' => 'search',
525  'placeholder' => wfMessage( 'searchsuggest-search' )->text(),
526  ];
527  $realAttrs = array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
528  return Html::element( 'input', $realAttrs );
529  }
530 
531  function makeSearchButton( $mode, $attrs = [] ) {
532  switch ( $mode ) {
533  case 'go':
534  case 'fulltext':
535  $realAttrs = [
536  'type' => 'submit',
537  'name' => $mode,
538  'value' => wfMessage( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
539  ];
540  $realAttrs = array_merge(
541  $realAttrs,
542  Linker::tooltipAndAccesskeyAttribs( "search-$mode" ),
543  $attrs
544  );
545  return Html::element( 'input', $realAttrs );
546  case 'image':
547  $buttonAttrs = [
548  'type' => 'submit',
549  'name' => 'button',
550  ];
551  $buttonAttrs = array_merge(
552  $buttonAttrs,
553  Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ),
554  $attrs
555  );
556  unset( $buttonAttrs['src'] );
557  unset( $buttonAttrs['alt'] );
558  unset( $buttonAttrs['width'] );
559  unset( $buttonAttrs['height'] );
560  $imgAttrs = [
561  'src' => $attrs['src'],
562  'alt' => $attrs['alt'] ?? wfMessage( 'searchbutton' )->text(),
563  'width' => $attrs['width'] ?? null,
564  'height' => $attrs['height'] ?? null,
565  ];
566  return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
567  default:
568  throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' );
569  }
570  }
571 
581  function getFooterLinks( $option = null ) {
582  $footerlinks = $this->get( 'footerlinks' );
583 
584  // Reduce footer links down to only those which are being used
585  $validFooterLinks = [];
586  foreach ( $footerlinks as $category => $links ) {
587  $validFooterLinks[$category] = [];
588  foreach ( $links as $link ) {
589  if ( isset( $this->data[$link] ) && $this->data[$link] ) {
590  $validFooterLinks[$category][] = $link;
591  }
592  }
593  if ( count( $validFooterLinks[$category] ) <= 0 ) {
594  unset( $validFooterLinks[$category] );
595  }
596  }
597 
598  if ( $option == 'flat' ) {
599  // fold footerlinks into a single array using a bit of trickery
600  $validFooterLinks = array_merge( ...array_values( $validFooterLinks ) );
601  }
602 
603  return $validFooterLinks;
604  }
605 
618  function getFooterIcons( $option = null ) {
619  // Generate additional footer icons
620  $footericons = $this->get( 'footericons' );
621 
622  if ( $option == 'icononly' ) {
623  // Unset any icons which don't have an image
624  foreach ( $footericons as &$footerIconsBlock ) {
625  foreach ( $footerIconsBlock as $footerIconKey => $footerIcon ) {
626  if ( !is_string( $footerIcon ) && !isset( $footerIcon['src'] ) ) {
627  unset( $footerIconsBlock[$footerIconKey] );
628  }
629  }
630  }
631  // Redo removal of any empty blocks
632  foreach ( $footericons as $footerIconsKey => &$footerIconsBlock ) {
633  if ( count( $footerIconsBlock ) <= 0 ) {
634  unset( $footericons[$footerIconsKey] );
635  }
636  }
637  } elseif ( $option == 'nocopyright' ) {
638  unset( $footericons['copyright']['copyright'] );
639  if ( count( $footericons['copyright'] ) <= 0 ) {
640  unset( $footericons['copyright'] );
641  }
642  }
643 
644  return $footericons;
645  }
646 
656  protected function getFooter( $iconStyle = 'icononly', $linkStyle = 'flat' ) {
657  $validFooterIcons = $this->getFooterIcons( $iconStyle );
658  $validFooterLinks = $this->getFooterLinks( $linkStyle );
659 
660  $html = '';
661 
662  if ( count( $validFooterIcons ) + count( $validFooterLinks ) > 0 ) {
663  $html .= Html::openElement( 'div', [
664  'id' => 'footer-bottom',
665  'role' => 'contentinfo',
666  'lang' => $this->get( 'userlang' ),
667  'dir' => $this->get( 'dir' )
668  ] );
669  $footerEnd = Html::closeElement( 'div' );
670  } else {
671  $footerEnd = '';
672  }
673  foreach ( $validFooterIcons as $blockName => $footerIcons ) {
674  $html .= Html::openElement( 'div', [
675  'id' => Sanitizer::escapeIdForAttribute( "f-{$blockName}ico" ),
676  'class' => 'footer-icons'
677  ] );
678  foreach ( $footerIcons as $icon ) {
679  $html .= $this->getSkin()->makeFooterIcon( $icon );
680  }
681  $html .= Html::closeElement( 'div' );
682  }
683  if ( count( $validFooterLinks ) > 0 ) {
684  $html .= Html::openElement( 'ul', [ 'id' => 'f-list', 'class' => 'footer-places' ] );
685  foreach ( $validFooterLinks as $aLink ) {
686  $html .= Html::rawElement(
687  'li',
688  [ 'id' => Sanitizer::escapeIdForAttribute( $aLink ) ],
689  $this->get( $aLink )
690  );
691  }
692  $html .= Html::closeElement( 'ul' );
693  }
694 
695  $html .= $this->getClear() . $footerEnd;
696 
697  return $html;
698  }
699 
706  protected function getClear() {
707  return Html::element( 'div', [ 'class' => 'visualClear' ] );
708  }
709 
725  public function getIndicators() {
726  $out = "<div class=\"mw-indicators mw-body-content\">\n";
727  foreach ( $this->data['indicators'] as $id => $content ) {
728  $out .= Html::rawElement(
729  'div',
730  [
731  'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
732  'class' => 'mw-indicator',
733  ],
734  $content
735  ) . "\n";
736  }
737  $out .= "</div>\n";
738  return $out;
739  }
740 
744  function printTrail() {
745  echo $this->getTrail();
746  }
747 
756  public function getTrail() {
757  return WrappedString::join( "\n", [
758  // @phan-suppress-next-line PhanTypeMismatchArgument
760  $this->get( 'bottomscripts' ),
761  $this->get( 'reporttime' )
762  ] );
763  }
764 }
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
getPersonalTools()
Create an array of personal tools items from the data in the quicktemplate stored by SkinTemplate...
Generic wrapper for template functions, with interface compatible with what we use of PHPTAL 0...
getFooter( $iconStyle='icononly', $linkStyle='flat')
Renderer for getFooterIcons and getFooterLinks.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:251
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
New base template for a skin&#39;s template extended from QuickTemplate this class features helper method...
getToolbox()
Create an array of common toolbox items from the data in the quicktemplate stored by SkinTemplate...
getTrail()
Get the basic end-page trail including bottomscripts, reporttime, and debug stuff.
makeLink( $key, $item, $options=[])
Makes a link, usually used by makeListItem to generate a link for an item in a list used in navigatio...
getSkin()
Get the Skin object related to this object.
getFooterIcons( $option=null)
Returns an array of footer icons filtered down by options relevant to how the skin wishes to display ...
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2021
getIndicators()
Get the suggested HTML for page status indicators: icons (or short text snippets) usually displayed i...
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:1295
getContext()
renderAfterPortlet( $name)
getFooterLinks( $option=null)
Returns an array of footerlinks trimmed down to only those footer links that are valid.
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
makeSearchInput( $attrs=[])
getSidebar( $options=[])
makeSearchButton( $mode, $attrs=[])
getClear()
Get a div with the core visualClear class, for clearing floats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$content
Definition: router.php:78
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2190
getMsg( $name,... $params)
Get a Message object with its context set.
static getDebugHTML(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition: MWDebug.php:445
msgWiki( $str)
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
getAfterPortlet( $name)
Allows extensions to hook into known portlets and add stuff to them.
printTrail()
Output getTrail.
makeListItem( $key, $item, $options=[])
Generates a list item for a navigation, portlet, portal, sidebar...