MediaWiki fundraising/REL1_35
VectorTemplate.php
Go to the documentation of this file.
1<?php
26
35 private const MENU_LABEL_KEYS = [
36 'cactions' => 'vector-more-actions',
37 'tb' => 'toolbox',
38 'personal' => 'personaltools',
39 'lang' => 'otherlanguages',
40 ];
42 private const MENU_TYPE_DEFAULT = 0;
44 private const MENU_TYPE_TABS = 1;
46 private const MENU_TYPE_DROPDOWN = 2;
47 private const MENU_TYPE_PORTAL = 3;
48
59 private const OPT_OUT_LINK_TRACKING_CODE = 'vctw1';
60
65
67 private $isLegacy;
68
74 public function __construct(
75 Config $config,
77 bool $isLegacy
78 ) {
79 parent::__construct( $config );
80
81 $this->templateParser = $templateParser;
82 $this->isLegacy = $isLegacy;
83 $this->templateRoot = $isLegacy ? 'skin-legacy' : 'skin';
84 }
85
89 private function getConfig() {
90 return $this->config;
91 }
92
98 protected function getTemplateParser() {
99 if ( $this->templateParser === null ) {
100 throw new \LogicException(
101 'TemplateParser has to be set first via setTemplateParser method'
102 );
103 }
105 }
106
112 private function getSkinData() : array {
113 // @phan-suppress-next-line PhanUndeclaredMethod
114 $contentNavigation = $this->getSkin()->getMenuProps();
115 $skin = $this->getSkin();
116 $out = $skin->getOutput();
117 $title = $out->getTitle();
118
119 // Naming conventions for Mustache parameters.
120 //
121 // Value type (first segment):
122 // - Prefix "is" or "has" for boolean values.
123 // - Prefix "msg-" for interface message text.
124 // - Prefix "html-" for raw HTML.
125 // - Prefix "data-" for an array of template parameters that should be passed directly
126 // to a template partial.
127 // - Prefix "array-" for lists of any values.
128 //
129 // Source of value (first or second segment)
130 // - Segment "page-" for data relating to the current page (e.g. Title, WikiPage, or OutputPage).
131 // - Segment "hook-" for any thing generated from a hook.
132 // It should be followed by the name of the hook in hyphenated lowercase.
133 //
134 // Conditionally used values must use null to indicate absence (not false or '').
135 $mainPageHref = Skin::makeMainPageUrl();
136 // From Skin::getNewtalks(). Always returns string, cast to null if empty.
137 $newTalksHtml = $skin->getNewtalks() ?: null;
138
139 // @phan-suppress-next-line PhanUndeclaredMethod
140 $commonSkinData = $skin->getTemplateData() + [
141 'html-headelement' => $out->headElement( $skin ),
142 'page-langcode' => $title->getPageViewLanguage()->getHtmlCode(),
143 'page-isarticle' => (bool)$out->isArticle(),
144
145 // Remember that the string '0' is a valid title.
146 // From OutputPage::getPageTitle, via ::setPageTitle().
147 'html-title' => $out->getPageTitle(),
148 'msg-tagline' => $skin->msg( 'tagline' )->text(),
149
150 'html-newtalk' => $newTalksHtml ? '<div class="usermessage">' . $newTalksHtml . '</div>' : '',
151
152 'msg-vector-jumptonavigation' => $skin->msg( 'vector-jumptonavigation' )->text(),
153 'msg-vector-jumptosearch' => $skin->msg( 'vector-jumptosearch' )->text(),
154
155 'html-printfooter' => $skin->printSource(),
156 'html-categories' => $skin->getCategories(),
157 'data-footer' => $this->getFooterData(),
158 'html-navigation-heading' => $skin->msg( 'navigation-heading' ),
159 'data-search-box' => $this->buildSearchProps(),
160
161 // Header
162 'data-logos' => ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() ),
163 'msg-sitetitle' => $skin->msg( 'sitetitle' )->text(),
164 'msg-sitesubtitle' => $skin->msg( 'sitesubtitle' )->text(),
165 'main-page-href' => $mainPageHref,
166
167 'data-sidebar' => $this->buildSidebar(),
168 'sidebar-visible' => $this->isSidebarVisible(),
169 'msg-vector-action-toggle-sidebar' => $skin->msg( 'vector-action-toggle-sidebar' )->text(),
170 ] + $this->getMenuProps();
171
172 // The following logic is unqiue to Vector (not used by legacy Vector) and
173 // is planned to be moved in a follow-up patch.
174 if ( !$this->isLegacy && $skin->getUser()->isLoggedIn() ) {
175 $commonSkinData['data-sidebar']['data-emphasized-sidebar-action'] = [
176 'href' => SpecialPage::getTitleFor(
177 'Preferences',
178 false,
179 'mw-prefsection-rendering-skin-skin-prefs'
180 )->getLinkURL( 'wprov=' . self::OPT_OUT_LINK_TRACKING_CODE ),
181 'text' => $skin->msg( 'vector-opt-out' )->text(),
182 'title' => $skin->msg( 'vector-opt-out-tooltip' )->text(),
183 ];
184 }
185
186 return $commonSkinData;
187 }
188
192 public function execute() {
193 $tp = $this->getTemplateParser();
194 echo $tp->processTemplate( $this->templateRoot, $this->getSkinData() );
195 }
196
201 private function getFooterData() : array {
202 $skin = $this->getSkin();
203 $footerRows = [];
204 foreach ( $this->getFooterLinks() as $category => $links ) {
205 $items = [];
206 $rowId = "footer-$category";
207
208 foreach ( $links as $link ) {
209 $items[] = [
210 'id' => "$rowId-$link",
211 'html' => $this->get( $link, '' ),
212 ];
213 }
214
215 $footerRows[] = [
216 'id' => $rowId,
217 'className' => null,
218 'array-items' => $items
219 ];
220 }
221
222 // If footer icons are enabled append to the end of the rows
223 $footerIcons = $this->getFooterIcons( 'icononly' );
224 if ( count( $footerIcons ) > 0 ) {
225 $items = [];
226 foreach ( $footerIcons as $blockName => $blockIcons ) {
227 $html = '';
228 foreach ( $blockIcons as $icon ) {
229 $html .= $skin->makeFooterIcon( $icon );
230 }
231 $items[] = [
232 'id' => 'footer-' . htmlspecialchars( $blockName ) . 'ico',
233 'html' => $html,
234 ];
235 }
236
237 $footerRows[] = [
238 'id' => 'footer-icons',
239 'className' => 'noprint',
240 'array-items' => $items,
241 ];
242 }
243
244 ob_start();
245 Hooks::run( 'VectorBeforeFooter', [], '1.35' );
246 $htmlHookVectorBeforeFooter = ob_get_contents();
247 ob_end_clean();
248
249 $data = [
250 'html-hook-vector-before-footer' => $htmlHookVectorBeforeFooter,
251 'array-footer-rows' => $footerRows,
252 ];
253
254 return $data;
255 }
256
262 private function isSidebarVisible() {
263 $skin = $this->getSkin();
264 if ( $skin->getUser()->isLoggedIn() ) {
265 $userPrefSidebarState = $skin->getUser()->getOption(
266 Constants::PREF_KEY_SIDEBAR_VISIBLE
267 );
268
269 $defaultLoggedinSidebarState = $this->getConfig()->get(
270 Constants::CONFIG_KEY_DEFAULT_SIDEBAR_VISIBLE_FOR_AUTHORISED_USER
271 );
272
273 // If the sidebar user preference has been set, return that value,
274 // if not, then the default sidebar state for logged-in users.
275 return ( $userPrefSidebarState !== null )
276 ? (bool)$userPrefSidebarState
277 : $defaultLoggedinSidebarState;
278 }
279 return $this->getConfig()->get(
280 Constants::CONFIG_KEY_DEFAULT_SIDEBAR_VISIBLE_FOR_ANONYMOUS_USER
281 );
282 }
283
289 private function buildSidebar() : array {
290 $skin = $this->getSkin();
291 $portals = $skin->buildSidebar();
292 $props = [];
293 $languages = null;
294
295 // Render portals
296 foreach ( $portals as $name => $content ) {
297 if ( $content === false ) {
298 continue;
299 }
300
301 // Numeric strings gets an integer when set as key, cast back - T73639
302 $name = (string)$name;
303
304 switch ( $name ) {
305 case 'SEARCH':
306 break;
307 case 'TOOLBOX':
308 $portal = $this->getMenuData(
309 'tb', $content, self::MENU_TYPE_PORTAL
310 );
311 // Run deprecated hook.
312 // Use SidebarBeforeOutput instead.
313 ob_start();
314 Hooks::run( 'VectorAfterToolbox', [], '1.35' );
315 $props[] = $portal + [
316 'html-hook-vector-after-toolbox' => ob_get_clean(),
317 ];
318 break;
319 case 'LANGUAGES':
320 $portal = $this->getMenuData(
321 'lang',
322 $content,
323 self::MENU_TYPE_PORTAL
324 );
325 // The language portal will be added provided either
326 // languages exist or there is a value in html-after-portal
327 // for example to show the add language wikidata link (T252800)
328 if ( count( $content ) || $portal['html-after-portal'] ) {
329 $languages = $portal;
330 }
331 break;
332 default:
333 // Historically some portals have been defined using HTML rather than arrays.
334 // Let's move away from that to a uniform definition.
335 if ( !is_array( $content ) ) {
336 $html = $content;
337 $content = [];
339 "`content` field in portal $name must be array."
340 . "Previously it could be a string but this is no longer supported.",
341 '1.35.0'
342 );
343 } else {
344 $html = false;
345 }
346 $portal = $this->getMenuData(
347 $name, $content, self::MENU_TYPE_PORTAL
348 );
349 if ( $html ) {
350 $portal['html-items'] .= $html;
351 }
352 $props[] = $portal;
353 break;
354 }
355 }
356
357 $firstPortal = $props[0] ?? null;
358 if ( $firstPortal ) {
359 $firstPortal[ 'class' ] .= ' portal-first';
360 }
361
362 return [
363 'has-logo' => $this->isLegacy,
364 'html-logo-attributes' => Xml::expandAttributes(
366 'class' => 'mw-wiki-logo',
367 'href' => Skin::makeMainPageUrl(),
368 ]
369 ),
370 'array-portals-rest' => array_slice( $props, 1 ),
371 'data-portals-first' => $firstPortal,
372 'data-portals-languages' => $languages,
373 ];
374 }
375
388 private function getMenuData(
389 string $label,
390 array $urls = [],
391 int $type = self::MENU_TYPE_DEFAULT,
392 array $options = [],
393 bool $setLabelToSelected = false
394 ) : array {
395 $skin = $this->getSkin();
396 $extraClasses = [
397 self::MENU_TYPE_DROPDOWN => 'vector-menu vector-menu-dropdown vectorMenu',
398 self::MENU_TYPE_TABS => 'vector-menu vector-menu-tabs vectorTabs',
399 self::MENU_TYPE_PORTAL => 'vector-menu vector-menu-portal portal',
400 self::MENU_TYPE_DEFAULT => 'vector-menu',
401 ];
402 // A list of classes to apply the list element and override the default behavior.
403 $listClasses = [
404 // `.menu` is on the portal for historic reasons.
405 // It should not be applied elsewhere per T253329.
406 self::MENU_TYPE_DROPDOWN => 'menu vector-menu-content-list',
407 ];
408 $isPortal = self::MENU_TYPE_PORTAL === $type;
409
410 // For some menu items, there is no language key corresponding with its menu key.
411 // These inconsitencies are captured in MENU_LABEL_KEYS
412 $msgObj = $skin->msg( self::MENU_LABEL_KEYS[ $label ] ?? $label );
413 $props = [
414 'id' => "p-$label",
415 'label-id' => "p-{$label}-label",
416 // If no message exists fallback to plain text (T252727)
417 'label' => $msgObj->exists() ? $msgObj->text() : $label,
418 'list-classes' => $listClasses[$type] ?? 'vector-menu-content-list',
419 'html-items' => '',
420 'is-dropdown' => self::MENU_TYPE_DROPDOWN === $type,
421 'html-tooltip' => Linker::tooltip( 'p-' . $label ),
422 ];
423
424 foreach ( $urls as $key => $item ) {
425 // Add CSS class 'collapsible' to all links EXCEPT watchstar.
426 if (
427 $key !== 'watch' && $key !== 'unwatch' &&
428 isset( $options['vector-collapsible'] ) && $options['vector-collapsible'] ) {
429 if ( !isset( $item['class'] ) ) {
430 $item['class'] = '';
431 }
432 $item['class'] = rtrim( 'collapsible ' . $item['class'], ' ' );
433 }
434 $props['html-items'] .= $this->getSkin()->makeListItem( $key, $item, $options );
435
436 // Check the class of the item for a `selected` class and if so, propagate the items
437 // label to the main label.
438 if ( $setLabelToSelected ) {
439 if ( isset( $item['class'] ) && stripos( $item['class'], 'selected' ) !== false ) {
440 $props['label'] = $item['text'];
441 }
442 }
443 }
444
445 $props['html-after-portal'] = $isPortal ? $this->getAfterPortlet( $label ) : '';
446
447 // Mark the portal as empty if it has no content
448 $class = ( count( $urls ) == 0 && !$props['html-after-portal'] )
449 ? 'vector-menu-empty emptyPortlet' : '';
450 $props['class'] = trim( "$class $extraClasses[$type]" );
451 return $props;
452 }
453
457 private function getMenuProps() : array {
458 // @phan-suppress-next-line PhanUndeclaredMethod
459 $contentNavigation = $this->getSkin()->getMenuProps();
460 $personalTools = $this->getPersonalTools();
461 $skin = $this->getSkin();
462
463 // For logged out users Vector shows a "Not logged in message"
464 // This should be upstreamed to core, with instructions for how to hide it for skins
465 // that do not want it.
466 // For now we create a dedicated list item to avoid having to sync the API internals
467 // of makeListItem.
468 if ( !$skin->getUser()->isLoggedIn() && User::groupHasPermission( '*', 'edit' ) ) {
469 $loggedIn =
470 Html::element( 'li',
471 [ 'id' => 'pt-anonuserpage' ],
472 $skin->msg( 'notloggedin' )->text()
473 );
474 } else {
475 $loggedIn = '';
476 }
477
478 // This code doesn't belong here, it belongs in the UniversalLanguageSelector
479 // It is here to workaround the fact that it wants to be the first item in the personal menus.
480 if ( array_key_exists( 'uls', $personalTools ) ) {
481 $uls = $skin->makeListItem( 'uls', $personalTools[ 'uls' ] );
482 unset( $personalTools[ 'uls' ] );
483 } else {
484 $uls = '';
485 }
486
487 $ptools = $this->getMenuData( 'personal', $personalTools );
488 // Append additional link items if present.
489 $ptools['html-items'] = $uls . $loggedIn . $ptools['html-items'];
490
491 return [
492 'data-personal-menu' => $ptools,
493 'data-namespace-tabs' => $this->getMenuData(
494 'namespaces',
495 $contentNavigation[ 'namespaces' ] ?? [],
496 self::MENU_TYPE_TABS
497 ),
498 'data-variants' => $this->getMenuData(
499 'variants',
500 $contentNavigation[ 'variants' ] ?? [],
501 self::MENU_TYPE_DROPDOWN,
502 [], true
503 ),
504 'data-page-actions' => $this->getMenuData(
505 'views',
506 $contentNavigation[ 'views' ] ?? [],
507 self::MENU_TYPE_TABS, [
508 'vector-collapsible' => true,
509 ]
510 ),
511 'data-page-actions-more' => $this->getMenuData(
512 'cactions',
513 $contentNavigation[ 'actions' ] ?? [],
514 self::MENU_TYPE_DROPDOWN
515 ),
516 ];
517 }
518
522 private function buildSearchProps() : array {
523 $config = $this->getConfig();
524 $skin = $this->getSkin();
525 $props = [
526 'form-action' => $config->get( 'Script' ),
527 'html-button-search-fallback' => $this->makeSearchButton(
528 'fulltext',
529 [ 'id' => 'mw-searchButton', 'class' => 'searchButton mw-fallbackSearchButton' ]
530 ),
531 'html-button-search' => $this->makeSearchButton(
532 'go',
533 [ 'id' => 'searchButton', 'class' => 'searchButton' ]
534 ),
535 'html-input' => $this->makeSearchInput( [ 'id' => 'searchInput' ] ),
536 'msg-search' => $skin->msg( 'search' ),
537 'page-title' => SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey(),
538 ];
539 return $props;
540 }
541}
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
$templateParser
New base template for a skin's template extended from QuickTemplate this class features helper method...
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition Hooks.php:134
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition Linker.php:2344
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2294
getSkin()
Get the Skin object related to this object.
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
QuickTemplate subclass for Vector.
getFooterData()
Get rows that make up the footer.
getTemplateParser()
The template parser might be undefined.
execute()
Renders the entire contents of the HTML page.
buildSidebar()
Render a series of portals.
string $templateRoot
File name of the root (master) template without folder path and extension.
TemplateParser $templateParser
isSidebarVisible()
Determines wheather the initial state of sidebar is visible on not.
getMenuData(string $label, array $urls=[], int $type=self::MENU_TYPE_DEFAULT, array $options=[], bool $setLabelToSelected=false)
__construct(Config $config, TemplateParser $templateParser, bool $isLegacy)
A namespace for Vector constants for internal Vector usage only.
Definition Constants.php:10
static expandAttributes( $attribs)
Given an array of ('attributename' => 'value'), it generates the code to set the XML attributes : att...
Definition Xml.php:67
Interface for configuration instances.
Definition Config.php:29
$content
Definition router.php:76