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