MediaWiki  master
ResourceLoaderWikiModule.php
Go to the documentation of this file.
1 <?php
27 use Wikimedia\Assert\Assert;
30 use Wikimedia\Timestamp\ConvertibleTimestamp;
31 
56 
57  // Origin defaults to users with sitewide authority
58  protected $origin = self::ORIGIN_USER_SITEWIDE;
59 
60  // In-process cache for title info, structured as an array
61  // [
62  // <batchKey> // Pipe-separated list of sorted keys from getPages
63  // => [
64  // <titleKey> => [ // Normalised title key
65  // 'page_len' => ..,
66  // 'page_latest' => ..,
67  // 'page_touched' => ..,
68  // ]
69  // ]
70  // ]
71  // @see self::fetchTitleInfo()
72  // @see self::makeTitleKey()
73  protected $titleInfo = [];
74 
75  // List of page names that contain CSS
76  protected $styles = [];
77 
78  // List of page names that contain JavaScript
79  protected $scripts = [];
80 
81  // Group of module
82  protected $group;
83 
88  public function __construct( array $options = null ) {
89  if ( $options === null ) {
90  return;
91  }
92 
93  foreach ( $options as $member => $option ) {
94  switch ( $member ) {
95  case 'styles':
96  case 'scripts':
97  case 'group':
98  case 'targets':
99  $this->{$member} = $option;
100  break;
101  }
102  }
103  }
104 
122  protected function getPages( ResourceLoaderContext $context ) {
123  $config = $this->getConfig();
124  $pages = [];
125 
126  // Filter out pages from origins not allowed by the current wiki configuration.
127  if ( $config->get( 'UseSiteJs' ) ) {
128  foreach ( $this->scripts as $script ) {
129  $pages[$script] = [ 'type' => 'script' ];
130  }
131  }
132 
133  if ( $config->get( 'UseSiteCss' ) ) {
134  foreach ( $this->styles as $style ) {
135  $pages[$style] = [ 'type' => 'style' ];
136  }
137  }
138 
139  return $pages;
140  }
141 
147  public function getGroup() {
148  return $this->group;
149  }
150 
165  protected function getDB() {
166  return wfGetDB( DB_REPLICA );
167  }
168 
175  protected function getContent( $titleText, ResourceLoaderContext $context ) {
176  $title = Title::newFromText( $titleText );
177  if ( !$title ) {
178  return null; // Bad title
179  }
180 
181  $content = $this->getContentObj( $title, $context );
182  if ( !$content ) {
183  return null; // No content found
184  }
185 
186  $handler = $content->getContentHandler();
187  if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
188  $format = CONTENT_FORMAT_CSS;
189  } elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) {
190  $format = CONTENT_FORMAT_JAVASCRIPT;
191  } else {
192  return null; // Bad content model
193  }
194 
195  return $content->serialize( $format );
196  }
197 
206  protected function getContentObj(
207  Title $title, ResourceLoaderContext $context, $maxRedirects = null
208  ) {
209  $overrideCallback = $context->getContentOverrideCallback();
210  $content = $overrideCallback ? call_user_func( $overrideCallback, $title ) : null;
211  if ( $content ) {
212  if ( !$content instanceof Content ) {
213  $this->getLogger()->error(
214  'Bad content override for "{title}" in ' . __METHOD__,
215  [ 'title' => $title->getPrefixedText() ]
216  );
217  return null;
218  }
219  } else {
220  $revision = MediaWikiServices::getInstance()
221  ->getRevisionLookup()
222  ->getKnownCurrentRevision( $title );
223  if ( !$revision ) {
224  return null;
225  }
226  $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
227 
228  if ( !$content ) {
229  $this->getLogger()->error(
230  'Failed to load content of JS/CSS page "{title}" in ' . __METHOD__,
231  [ 'title' => $title->getPrefixedText() ]
232  );
233  return null;
234  }
235  }
236 
237  if ( $content && $content->isRedirect() ) {
238  if ( $maxRedirects === null ) {
239  $maxRedirects = $this->getConfig()->get( 'MaxRedirects' ) ?: 0;
240  }
241  if ( $maxRedirects > 0 ) {
242  $newTitle = $content->getRedirectTarget();
243  return $newTitle ? $this->getContentObj( $newTitle, $context, $maxRedirects - 1 ) : null;
244  }
245  }
246 
247  return $content;
248  }
249 
255  $overrideCallback = $context->getContentOverrideCallback();
256  if ( $overrideCallback && $this->getSource() === 'local' ) {
257  foreach ( $this->getPages( $context ) as $page => $info ) {
258  $title = Title::newFromText( $page );
259  if ( $title && call_user_func( $overrideCallback, $title ) !== null ) {
260  return true;
261  }
262  }
263  }
264 
265  return parent::shouldEmbedModule( $context );
266  }
267 
273  $scripts = '';
274  foreach ( $this->getPages( $context ) as $titleText => $options ) {
275  if ( $options['type'] !== 'script' ) {
276  continue;
277  }
278  $script = $this->getContent( $titleText, $context );
279  if ( strval( $script ) !== '' ) {
280  $script = $this->validateScriptFile( $titleText, $script );
281  $scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n";
282  }
283  }
284  return $scripts;
285  }
286 
292  $styles = [];
293  foreach ( $this->getPages( $context ) as $titleText => $options ) {
294  if ( $options['type'] !== 'style' ) {
295  continue;
296  }
297  $media = $options['media'] ?? 'all';
298  $style = $this->getContent( $titleText, $context );
299  if ( strval( $style ) === '' ) {
300  continue;
301  }
302  if ( $this->getFlip( $context ) ) {
303  $style = CSSJanus::transform( $style, true, false );
304  }
305  $style = MemoizedCallable::call( 'CSSMin::remap',
306  [ $style, false, $this->getConfig()->get( 'ScriptPath' ), true ] );
307  if ( !isset( $styles[$media] ) ) {
308  $styles[$media] = [];
309  }
310  $style = ResourceLoader::makeComment( $titleText ) . $style;
311  $styles[$media][] = $style;
312  }
313  return $styles;
314  }
315 
326  public function enableModuleContentVersion() {
327  return false;
328  }
329 
335  $summary = parent::getDefinitionSummary( $context );
336  $summary[] = [
337  'pages' => $this->getPages( $context ),
338  // Includes meta data of current revisions
339  'titleInfo' => $this->getTitleInfo( $context ),
340  ];
341  return $summary;
342  }
343 
349  $revisions = $this->getTitleInfo( $context );
350 
351  // If a module has dependencies it cannot be empty. An empty array will be cast to false
352  if ( $this->getDependencies() ) {
353  return false;
354  }
355  // For user modules, don't needlessly load if there are no non-empty pages
356  if ( $this->getGroup() === 'user' ) {
357  foreach ( $revisions as $revision ) {
358  if ( $revision['page_len'] > 0 ) {
359  // At least one non-empty page, module should be loaded
360  return false;
361  }
362  }
363  return true;
364  }
365 
366  // T70488: For other modules (i.e. ones that are called in cached html output) only check
367  // page existance. This ensures that, if some pages in a module are temporarily blanked,
368  // we don't end omit the module's script or link tag on some pages.
369  return count( $revisions ) === 0;
370  }
371 
372  private function setTitleInfo( $batchKey, array $titleInfo ) {
373  $this->titleInfo[$batchKey] = $titleInfo;
374  }
375 
376  private static function makeTitleKey( LinkTarget $title ) {
377  // Used for keys in titleInfo.
378  return "{$title->getNamespace()}:{$title->getDBkey()}";
379  }
380 
387  $dbr = $this->getDB();
388 
389  $pageNames = array_keys( $this->getPages( $context ) );
390  sort( $pageNames );
391  $batchKey = implode( '|', $pageNames );
392  if ( !isset( $this->titleInfo[$batchKey] ) ) {
393  $this->titleInfo[$batchKey] = static::fetchTitleInfo( $dbr, $pageNames, __METHOD__ );
394  }
395 
396  $titleInfo = $this->titleInfo[$batchKey];
397 
398  // Override the title info from the overrides, if any
399  $overrideCallback = $context->getContentOverrideCallback();
400  if ( $overrideCallback ) {
401  foreach ( $pageNames as $page ) {
402  $title = Title::newFromText( $page );
403  $content = $title ? call_user_func( $overrideCallback, $title ) : null;
404  if ( $content !== null ) {
405  $titleInfo[$title->getPrefixedText()] = [
406  'page_len' => $content->getSize(),
407  'page_latest' => 'TBD', // None available
408  'page_touched' => ConvertibleTimestamp::now( TS_MW ),
409  ];
410  }
411  }
412  }
413 
414  return $titleInfo;
415  }
416 
423  protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) {
424  $titleInfo = [];
425  $batch = new LinkBatch;
426  foreach ( $pages as $titleText ) {
427  $title = Title::newFromText( $titleText );
428  if ( $title ) {
429  // Page name may be invalid if user-provided (e.g. gadgets)
430  $batch->addObj( $title );
431  }
432  }
433  if ( !$batch->isEmpty() ) {
434  $res = $db->select( 'page',
435  // Include page_touched to allow purging if cache is poisoned (T117587, T113916)
436  [ 'page_namespace', 'page_title', 'page_touched', 'page_len', 'page_latest' ],
437  $batch->constructSet( 'page', $db ),
438  $fname
439  );
440  foreach ( $res as $row ) {
441  // Avoid including ids or timestamps of revision/page tables so
442  // that versions are not wasted
443  $title = new TitleValue( (int)$row->page_namespace, $row->page_title );
445  'page_len' => $row->page_len,
446  'page_latest' => $row->page_latest,
447  'page_touched' => $row->page_touched,
448  ];
449  }
450  }
451  return $titleInfo;
452  }
453 
460  public static function preloadTitleInfo(
461  ResourceLoaderContext $context, IDatabase $db, array $moduleNames
462  ) {
463  $rl = $context->getResourceLoader();
464  // getDB() can be overridden to point to a foreign database.
465  // For now, only preload local. In the future, we could preload by wikiID.
466  $allPages = [];
468  $wikiModules = [];
469  foreach ( $moduleNames as $name ) {
470  $module = $rl->getModule( $name );
471  if ( $module instanceof self ) {
472  $mDB = $module->getDB();
473  // Subclasses may implement getDB differently
474  if ( $mDB->getDomainID() === $db->getDomainID() ) {
475  $wikiModules[] = $module;
476  $allPages += $module->getPages( $context );
477  }
478  }
479  }
480 
481  if ( !$wikiModules ) {
482  // Nothing to preload
483  return;
484  }
485 
486  $pageNames = array_keys( $allPages );
487  sort( $pageNames );
488  $hash = sha1( implode( '|', $pageNames ) );
489 
490  // Avoid Zend bug where "static::" does not apply LSB in the closure
491  $func = [ static::class, 'fetchTitleInfo' ];
492  $fname = __METHOD__;
493 
494  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
495  $allInfo = $cache->getWithSetCallback(
496  $cache->makeGlobalKey( 'resourceloader-titleinfo', $db->getDomainID(), $hash ),
497  $cache::TTL_HOUR,
498  function ( $curVal, &$ttl, array &$setOpts ) use ( $func, $pageNames, $db, $fname ) {
499  $setOpts += Database::getCacheSetOptions( $db );
500 
501  return call_user_func( $func, $db, $pageNames, $fname );
502  },
503  [
504  'checkKeys' => [
505  $cache->makeGlobalKey( 'resourceloader-titleinfo', $db->getDomainID() ) ]
506  ]
507  );
508 
509  foreach ( $wikiModules as $wikiModule ) {
510  $pages = $wikiModule->getPages( $context );
511  // Before we intersect, map the names to canonical form (T145673).
512  $intersect = [];
513  foreach ( $pages as $pageName => $unused ) {
514  $title = Title::newFromText( $pageName );
515  if ( $title ) {
516  $intersect[ self::makeTitleKey( $title ) ] = 1;
517  } else {
518  // Page name may be invalid if user-provided (e.g. gadgets)
519  $rl->getLogger()->info(
520  'Invalid wiki page title "{title}" in ' . __METHOD__,
521  [ 'title' => $pageName ]
522  );
523  }
524  }
525  $info = array_intersect_key( $allInfo, $intersect );
526  $pageNames = array_keys( $pages );
527  sort( $pageNames );
528  $batchKey = implode( '|', $pageNames );
529  $wikiModule->setTitleInfo( $batchKey, $info );
530  }
531  }
532 
543  public static function invalidateModuleCache(
544  Title $title, ?Revision $old, ?Revision $new, $domain
545  ) {
546  static $formats = [ CONTENT_FORMAT_CSS, CONTENT_FORMAT_JAVASCRIPT ];
547 
548  Assert::parameterType( 'string', $domain, '$domain' );
549 
550  // TODO: MCR: differentiate between page functionality and content model!
551  // Not all pages containing CSS or JS have to be modules! [PageType]
552  if ( $old && in_array( $old->getContentFormat(), $formats ) ) {
553  $purge = true;
554  } elseif ( $new && in_array( $new->getContentFormat(), $formats ) ) {
555  $purge = true;
556  } else {
557  $purge = ( $title->isSiteConfigPage() || $title->isUserConfigPage() );
558  }
559 
560  if ( $purge ) {
561  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
562  $key = $cache->makeGlobalKey( 'resourceloader-titleinfo', $domain );
563  $cache->touchCheckKey( $key );
564  }
565  }
566 
571  public function getType() {
572  // Check both because subclasses don't always pass pages via the constructor,
573  // they may also override getPages() instead, in which case we should keep
574  // defaulting to LOAD_GENERAL and allow them to override getType() separately.
575  return ( $this->styles && !$this->scripts ) ? self::LOAD_STYLES : self::LOAD_GENERAL;
576  }
577 }
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:49
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:332
ResourceLoaderWikiModule\$styles
$styles
Definition: ResourceLoaderWikiModule.php:76
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
ResourceLoaderModule\getFlip
getFlip(ResourceLoaderContext $context)
Definition: ResourceLoaderModule.php:147
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:35
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:137
ResourceLoaderWikiModule\getDB
getDB()
Get the Database handle used for computing the module version.
Definition: ResourceLoaderWikiModule.php:165
ResourceLoaderWikiModule\getStyles
getStyles(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:291
ResourceLoaderWikiModule
Abstraction for ResourceLoader modules which pull from wiki pages.
Definition: ResourceLoaderWikiModule.php:55
ResourceLoaderWikiModule\getGroup
getGroup()
Get group name.
Definition: ResourceLoaderWikiModule.php:147
ResourceLoaderWikiModule\getScript
getScript(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:272
CONTENT_FORMAT_CSS
const CONTENT_FORMAT_CSS
Definition: Defines.php:243
ResourceLoaderWikiModule\setTitleInfo
setTitleInfo( $batchKey, array $titleInfo)
Definition: ResourceLoaderWikiModule.php:372
$res
$res
Definition: testCompression.php:57
ResourceLoaderWikiModule\isKnownEmpty
isKnownEmpty(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:348
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
ResourceLoaderWikiModule\$titleInfo
$titleInfo
Definition: ResourceLoaderWikiModule.php:73
$dbr
$dbr
Definition: testCompression.php:54
MemoizedCallable\call
static call( $callable, array $args=[], $ttl=3600)
Shortcut method for creating a MemoizedCallable and invoking it with the specified arguments.
Definition: MemoizedCallable.php:157
Revision
Definition: Revision.php:40
ResourceLoaderWikiModule\getType
getType()
Definition: ResourceLoaderWikiModule.php:571
ResourceLoaderModule\getLogger
getLogger()
Definition: ResourceLoaderModule.php:239
ResourceLoaderWikiModule\preloadTitleInfo
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
Definition: ResourceLoaderWikiModule.php:460
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2500
ResourceLoaderWikiModule\enableModuleContentVersion
enableModuleContentVersion()
Disable module content versioning.
Definition: ResourceLoaderWikiModule.php:326
ResourceLoaderWikiModule\getContentObj
getContentObj(Title $title, ResourceLoaderContext $context, $maxRedirects=null)
Definition: ResourceLoaderWikiModule.php:206
$title
$title
Definition: testCompression.php:38
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, ?Revision $old, ?Revision $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:543
ResourceLoaderWikiModule\fetchTitleInfo
static fetchTitleInfo(IDatabase $db, array $pages, $fname=__METHOD__)
Definition: ResourceLoaderWikiModule.php:423
$content
$content
Definition: router.php:78
ResourceLoaderModule\$name
string null $name
Module name.
Definition: ResourceLoaderModule.php:52
ResourceLoaderModule\getDependencies
getDependencies(ResourceLoaderContext $context=null)
Get a list of modules this module depends on.
Definition: ResourceLoaderModule.php:366
Wikimedia\Rdbms\IDatabase\getDomainID
getDomainID()
Return the currently selected domain ID.
ResourceLoaderWikiModule\$scripts
$scripts
Definition: ResourceLoaderWikiModule.php:79
ResourceLoaderWikiModule\__construct
__construct(array $options=null)
Definition: ResourceLoaderWikiModule.php:88
ResourceLoaderModule\validateScriptFile
validateScriptFile( $fileName, $contents)
Validate a given script file; if valid returns the original source.
Definition: ResourceLoaderModule.php:912
Revision\getContentFormat
getContentFormat()
Returns the content format for the main slot of this revision.
Definition: Revision.php:805
$context
$context
Definition: load.php:43
Content
Base interface for content objects.
Definition: Content.php:34
ResourceLoaderWikiModule\$origin
$origin
Definition: ResourceLoaderWikiModule.php:58
ResourceLoaderWikiModule\$group
$group
Definition: ResourceLoaderWikiModule.php:82
Title
Represents a title within MediaWiki.
Definition: Title.php:42
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:36
ResourceLoaderModule\$config
Config $config
Definition: ResourceLoaderModule.php:38
$cache
$cache
Definition: mcc.php:33
ResourceLoaderWikiModule\shouldEmbedModule
shouldEmbedModule(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:254
ResourceLoaderWikiModule\getContent
getContent( $titleText, ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:175
ResourceLoaderWikiModule\getDefinitionSummary
getDefinitionSummary(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:334
ResourceLoaderWikiModule\makeTitleKey
static makeTitleKey(LinkTarget $title)
Definition: ResourceLoaderWikiModule.php:376
Wikimedia\Rdbms\IDatabase\select
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
ResourceLoaderModule\getSource
getSource()
Get the source of this module.
Definition: ResourceLoaderModule.php:349
ResourceLoaderWikiModule\getPages
getPages(ResourceLoaderContext $context)
Subclasses should return an associative array of resources in the module.
Definition: ResourceLoaderWikiModule.php:122
CONTENT_FORMAT_JAVASCRIPT
const CONTENT_FORMAT_JAVASCRIPT
Definition: Defines.php:241
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:210
ResourceLoaderWikiModule\getTitleInfo
getTitleInfo(ResourceLoaderContext $context)
Get the information about the wiki pages for a given context.
Definition: ResourceLoaderWikiModule.php:386
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:37