MediaWiki  master
ResourceLoaderWikiModule.php
Go to the documentation of this file.
1 <?php
28 use Wikimedia\Minify\CSSMin;
31 use Wikimedia\Timestamp\ConvertibleTimestamp;
32 
58  protected $origin = self::ORIGIN_USER_SITEWIDE;
59 
76  protected $titleInfo = [];
77 
79  protected $styles = [];
80 
82  protected $scripts = [];
83 
85  protected $group;
86 
91  public function __construct( array $options = null ) {
92  if ( $options === null ) {
93  return;
94  }
95 
96  foreach ( $options as $member => $option ) {
97  switch ( $member ) {
98  case 'styles':
99  case 'scripts':
100  case 'group':
101  case 'targets':
102  $this->{$member} = $option;
103  break;
104  }
105  }
106  }
107 
125  protected function getPages( ResourceLoaderContext $context ) {
126  $config = $this->getConfig();
127  $pages = [];
128 
129  // Filter out pages from origins not allowed by the current wiki configuration.
130  if ( $config->get( 'UseSiteJs' ) ) {
131  foreach ( $this->scripts as $script ) {
132  $pages[$script] = [ 'type' => 'script' ];
133  }
134  }
135 
136  if ( $config->get( 'UseSiteCss' ) ) {
137  foreach ( $this->styles as $style ) {
138  $pages[$style] = [ 'type' => 'style' ];
139  }
140  }
141 
142  return $pages;
143  }
144 
150  public function getGroup() {
151  return $this->group;
152  }
153 
168  protected function getDB() {
169  return wfGetDB( DB_REPLICA );
170  }
171 
178  protected function getContent( $titleText, ResourceLoaderContext $context ) {
179  $pageStore = MediaWikiServices::getInstance()->getPageStore();
180  $title = $pageStore->getPageByText( $titleText );
181  if ( !$title ) {
182  return null; // Bad title
183  }
184 
185  $content = $this->getContentObj( $title, $context );
186  if ( !$content ) {
187  return null; // No content found
188  }
189 
190  $handler = $content->getContentHandler();
191  if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
192  $format = CONTENT_FORMAT_CSS;
193  } elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) {
194  $format = CONTENT_FORMAT_JAVASCRIPT;
195  } else {
196  return null; // Bad content model
197  }
198 
199  return $content->serialize( $format );
200  }
201 
210  protected function getContentObj(
211  PageIdentity $page, ResourceLoaderContext $context, $maxRedirects = null
212  ) {
213  $overrideCallback = $context->getContentOverrideCallback();
214  $content = $overrideCallback ? call_user_func( $overrideCallback, $page ) : null;
215  if ( $content ) {
216  if ( !$content instanceof Content ) {
217  $this->getLogger()->error(
218  'Bad content override for "{title}" in ' . __METHOD__,
219  [ 'title' => (string)$page ]
220  );
221  return null;
222  }
223  } else {
224  $revision = MediaWikiServices::getInstance()
225  ->getRevisionLookup()
226  ->getKnownCurrentRevision( $page );
227  if ( !$revision ) {
228  return null;
229  }
230  $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
231 
232  if ( !$content ) {
233  $this->getLogger()->error(
234  'Failed to load content of JS/CSS page "{title}" in ' . __METHOD__,
235  [ 'title' => (string)$page ]
236  );
237  return null;
238  }
239  }
240 
241  if ( $content->isRedirect() ) {
242  if ( $maxRedirects === null ) {
243  $maxRedirects = $this->getConfig()->get( 'MaxRedirects' ) ?: 0;
244  }
245  if ( $maxRedirects > 0 ) {
246  $newTitle = $content->getRedirectTarget();
247  return $newTitle ? $this->getContentObj( $newTitle, $context, $maxRedirects - 1 ) : null;
248  }
249  }
250 
251  return $content;
252  }
253 
258  public function shouldEmbedModule( ResourceLoaderContext $context ) {
259  $overrideCallback = $context->getContentOverrideCallback();
260  if ( $overrideCallback && $this->getSource() === 'local' ) {
261  foreach ( $this->getPages( $context ) as $page => $info ) {
262  $title = Title::newFromText( $page );
263  if ( $title && call_user_func( $overrideCallback, $title ) !== null ) {
264  return true;
265  }
266  }
267  }
268 
269  return parent::shouldEmbedModule( $context );
270  }
271 
276  public function getScript( ResourceLoaderContext $context ) {
277  $scripts = '';
278  foreach ( $this->getPages( $context ) as $titleText => $options ) {
279  if ( $options['type'] !== 'script' ) {
280  continue;
281  }
282  $script = $this->getContent( $titleText, $context );
283  if ( strval( $script ) !== '' ) {
284  $script = $this->validateScriptFile( $titleText, $script );
285  $scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n";
286  }
287  }
288  return $scripts;
289  }
290 
295  public function getStyles( ResourceLoaderContext $context ) {
296  $styles = [];
297  foreach ( $this->getPages( $context ) as $titleText => $options ) {
298  if ( $options['type'] !== 'style' ) {
299  continue;
300  }
301  $media = $options['media'] ?? 'all';
302  $style = $this->getContent( $titleText, $context );
303  if ( strval( $style ) === '' ) {
304  continue;
305  }
306  if ( $this->getFlip( $context ) ) {
307  $style = CSSJanus::transform( $style, true, false );
308  }
309  $remoteDir = $this->getConfig()->get( 'ScriptPath' );
310  if ( $remoteDir === '' ) {
311  // When the site is configured with the script path at the
312  // document root, MediaWiki uses an empty string but that is
313  // not a valid URI path. Expand to a slash to avoid fatals
314  // later in CSSMin::resolveUrl().
315  // See also ResourceLoaderFilePath::extractBasePaths, T282280.
316  $remoteDir = '/';
317  }
318 
319  $style = MemoizedCallable::call(
320  [ CSSMin::class, 'remap' ],
321  [ $style, false, $remoteDir, true ]
322  );
323  if ( !isset( $styles[$media] ) ) {
324  $styles[$media] = [];
325  }
326  $style = ResourceLoader::makeComment( $titleText ) . $style;
327  $styles[$media][] = $style;
328  }
329  return $styles;
330  }
331 
342  public function enableModuleContentVersion() {
343  return false;
344  }
345 
350  public function getDefinitionSummary( ResourceLoaderContext $context ) {
351  $summary = parent::getDefinitionSummary( $context );
352  $summary[] = [
353  'pages' => $this->getPages( $context ),
354  // Includes meta data of current revisions
355  'titleInfo' => $this->getTitleInfo( $context ),
356  ];
357  return $summary;
358  }
359 
364  public function isKnownEmpty( ResourceLoaderContext $context ) {
365  $revisions = $this->getTitleInfo( $context );
366 
367  // If a module has dependencies it cannot be empty. An empty array will be cast to false
368  if ( $this->getDependencies() ) {
369  return false;
370  }
371  // For user modules, don't needlessly load if there are no non-empty pages
372  if ( $this->getGroup() === 'user' ) {
373  foreach ( $revisions as $revision ) {
374  if ( $revision['page_len'] > 0 ) {
375  // At least one non-empty page, module should be loaded
376  return false;
377  }
378  }
379  return true;
380  }
381 
382  // T70488: For other modules (i.e. ones that are called in cached html output) only check
383  // page existance. This ensures that, if some pages in a module are temporarily blanked,
384  // we don't end omit the module's script or link tag on some pages.
385  return count( $revisions ) === 0;
386  }
387 
388  private function setTitleInfo( $batchKey, array $titleInfo ) {
389  $this->titleInfo[$batchKey] = $titleInfo;
390  }
391 
392  private static function makeTitleKey( LinkTarget $title ) {
393  // Used for keys in titleInfo.
394  return "{$title->getNamespace()}:{$title->getDBkey()}";
395  }
396 
402  protected function getTitleInfo( ResourceLoaderContext $context ) {
403  $dbr = $this->getDB();
404 
405  $pageNames = array_keys( $this->getPages( $context ) );
406  sort( $pageNames );
407  $batchKey = implode( '|', $pageNames );
408  if ( !isset( $this->titleInfo[$batchKey] ) ) {
409  $this->titleInfo[$batchKey] = static::fetchTitleInfo( $dbr, $pageNames, __METHOD__ );
410  }
411 
412  $titleInfo = $this->titleInfo[$batchKey];
413 
414  // Override the title info from the overrides, if any
415  $overrideCallback = $context->getContentOverrideCallback();
416  if ( $overrideCallback ) {
417  foreach ( $pageNames as $page ) {
418  $title = Title::newFromText( $page );
419  $content = $title ? call_user_func( $overrideCallback, $title ) : null;
420  if ( $content !== null ) {
421  $titleInfo[$title->getPrefixedText()] = [
422  'page_len' => $content->getSize(),
423  'page_latest' => 'TBD', // None available
424  'page_touched' => ConvertibleTimestamp::now( TS_MW ),
425  ];
426  }
427  }
428  }
429 
430  return $titleInfo;
431  }
432 
439  protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) {
440  $titleInfo = [];
441  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
442  $batch = $linkBatchFactory->newLinkBatch();
443  foreach ( $pages as $titleText ) {
444  $title = Title::newFromText( $titleText );
445  if ( $title ) {
446  // Page name may be invalid if user-provided (e.g. gadgets)
447  $batch->addObj( $title );
448  }
449  }
450  if ( !$batch->isEmpty() ) {
451  $res = $db->select( 'page',
452  // Include page_touched to allow purging if cache is poisoned (T117587, T113916)
453  [ 'page_namespace', 'page_title', 'page_touched', 'page_len', 'page_latest' ],
454  $batch->constructSet( 'page', $db ),
455  $fname
456  );
457  foreach ( $res as $row ) {
458  // Avoid including ids or timestamps of revision/page tables so
459  // that versions are not wasted
460  $title = new TitleValue( (int)$row->page_namespace, $row->page_title );
462  'page_len' => $row->page_len,
463  'page_latest' => $row->page_latest,
464  'page_touched' => $row->page_touched,
465  ];
466  }
467  }
468  return $titleInfo;
469  }
470 
477  public static function preloadTitleInfo(
478  ResourceLoaderContext $context, IDatabase $db, array $moduleNames
479  ) {
480  $rl = $context->getResourceLoader();
481  // getDB() can be overridden to point to a foreign database.
482  // For now, only preload local. In the future, we could preload by wikiID.
483  $allPages = [];
485  $wikiModules = [];
486  foreach ( $moduleNames as $name ) {
487  $module = $rl->getModule( $name );
488  if ( $module instanceof self ) {
489  $mDB = $module->getDB();
490  // Subclasses may implement getDB differently
491  if ( $mDB->getDomainID() === $db->getDomainID() ) {
492  $wikiModules[] = $module;
493  $allPages += $module->getPages( $context );
494  }
495  }
496  }
497 
498  if ( !$wikiModules ) {
499  // Nothing to preload
500  return;
501  }
502 
503  $pageNames = array_keys( $allPages );
504  sort( $pageNames );
505  $hash = sha1( implode( '|', $pageNames ) );
506 
507  // Avoid Zend bug where "static::" does not apply LSB in the closure
508  $func = [ static::class, 'fetchTitleInfo' ];
509  $fname = __METHOD__;
510 
511  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
512  $allInfo = $cache->getWithSetCallback(
513  $cache->makeGlobalKey( 'resourceloader-titleinfo', $db->getDomainID(), $hash ),
514  $cache::TTL_HOUR,
515  static function ( $curVal, &$ttl, array &$setOpts ) use ( $func, $pageNames, $db, $fname ) {
516  $setOpts += Database::getCacheSetOptions( $db );
517 
518  return call_user_func( $func, $db, $pageNames, $fname );
519  },
520  [
521  'checkKeys' => [
522  $cache->makeGlobalKey( 'resourceloader-titleinfo', $db->getDomainID() ) ]
523  ]
524  );
525 
526  foreach ( $wikiModules as $wikiModule ) {
527  $pages = $wikiModule->getPages( $context );
528  // Before we intersect, map the names to canonical form (T145673).
529  $intersect = [];
530  foreach ( $pages as $pageName => $unused ) {
531  $title = Title::newFromText( $pageName );
532  if ( $title ) {
533  $intersect[ self::makeTitleKey( $title ) ] = 1;
534  } else {
535  // Page name may be invalid if user-provided (e.g. gadgets)
536  $rl->getLogger()->info(
537  'Invalid wiki page title "{title}" in ' . __METHOD__,
538  [ 'title' => $pageName ]
539  );
540  }
541  }
542  $info = array_intersect_key( $allInfo, $intersect );
543  $pageNames = array_keys( $pages );
544  sort( $pageNames );
545  $batchKey = implode( '|', $pageNames );
546  $wikiModule->setTitleInfo( $batchKey, $info );
547  }
548  }
549 
560  public static function invalidateModuleCache(
561  PageIdentity $page,
562  ?RevisionRecord $old,
563  ?RevisionRecord $new,
564  string $domain
565  ) {
566  static $models = [ CONTENT_MODEL_CSS, CONTENT_MODEL_JAVASCRIPT ];
567 
568  $purge = false;
569  // TODO: MCR: differentiate between page functionality and content model!
570  // Not all pages containing CSS or JS have to be modules! [PageType]
571  if ( $old ) {
572  $oldModel = $old->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
573  if ( in_array( $oldModel, $models ) ) {
574  $purge = true;
575  }
576  }
577 
578  if ( !$purge && $new ) {
579  $newModel = $new->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
580  if ( in_array( $newModel, $models ) ) {
581  $purge = true;
582  }
583  }
584 
585  if ( !$purge ) {
587  $purge = ( $title->isSiteConfigPage() || $title->isUserConfigPage() );
588  }
589 
590  if ( $purge ) {
591  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
592  $key = $cache->makeGlobalKey( 'resourceloader-titleinfo', $domain );
593  $cache->touchCheckKey( $key );
594  }
595  }
596 
601  public function getType() {
602  // Check both because subclasses don't always pass pages via the constructor,
603  // they may also override getPages() instead, in which case we should keep
604  // defaulting to LOAD_GENERAL and allow them to override getType() separately.
605  return ( $this->styles && !$this->scripts ) ? self::LOAD_STYLES : self::LOAD_GENERAL;
606  }
607 }
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:34
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
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:383
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
ResourceLoaderModule\getFlip
getFlip(ResourceLoaderContext $context)
Definition: ResourceLoaderModule.php:169
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
ResourceLoaderContext\getResourceLoader
getResourceLoader()
Definition: ResourceLoaderContext.php:150
ResourceLoaderWikiModule\getDB
getDB()
Get the Database handle used for computing the module version.
Definition: ResourceLoaderWikiModule.php:168
ResourceLoaderWikiModule\getStyles
getStyles(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:295
ResourceLoaderWikiModule
Abstraction for ResourceLoader modules which pull from wiki pages.
Definition: ResourceLoaderWikiModule.php:56
ResourceLoaderContext\getContentOverrideCallback
getContentOverrideCallback()
Return the replaced-content mapping callback.
Definition: ResourceLoaderContext.php:357
ResourceLoaderWikiModule\getGroup
getGroup()
Get group name.
Definition: ResourceLoaderWikiModule.php:150
ResourceLoaderWikiModule\getScript
getScript(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:276
ResourceLoaderWikiModule\setTitleInfo
setTitleInfo( $batchKey, array $titleInfo)
Definition: ResourceLoaderWikiModule.php:388
CONTENT_FORMAT_CSS
const CONTENT_FORMAT_CSS
For CSS pages.
Definition: Defines.php:228
$res
$res
Definition: testCompression.php:57
ResourceLoaderWikiModule\isKnownEmpty
isKnownEmpty(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:364
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
ResourceLoaderWikiModule\$origin
string $origin
Origin defaults to users with sitewide authority.
Definition: ResourceLoaderWikiModule.php:58
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:332
$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:156
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(PageIdentity $page, ?RevisionRecord $old, ?RevisionRecord $new, string $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:560
ResourceLoaderWikiModule\getType
getType()
Definition: ResourceLoaderWikiModule.php:601
ResourceLoaderModule\getLogger
getLogger()
Definition: ResourceLoaderModule.php:262
ResourceLoaderWikiModule\preloadTitleInfo
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
Definition: ResourceLoaderWikiModule.php:477
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:2202
ResourceLoaderWikiModule\enableModuleContentVersion
enableModuleContentVersion()
Disable module content versioning.
Definition: ResourceLoaderWikiModule.php:342
ResourceLoaderWikiModule\$titleInfo
array $titleInfo
In-process cache for title info, structured as an array [ <batchKey> // Pipe-separated list of sorted...
Definition: ResourceLoaderWikiModule.php:76
ResourceLoaderWikiModule\$styles
array $styles
List of page names that contain CSS.
Definition: ResourceLoaderWikiModule.php:79
$title
$title
Definition: testCompression.php:38
ResourceLoader\makeComment
static makeComment( $text)
Generate a CSS or JS comment block.
Definition: ResourceLoader.php:1054
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ResourceLoaderWikiModule\$scripts
array $scripts
List of page names that contain JavaScript.
Definition: ResourceLoaderWikiModule.php:82
ResourceLoaderWikiModule\fetchTitleInfo
static fetchTitleInfo(IDatabase $db, array $pages, $fname=__METHOD__)
Definition: ResourceLoaderWikiModule.php:439
$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:425
Wikimedia\Rdbms\IDatabase\getDomainID
getDomainID()
Return the currently selected domain ID.
ResourceLoaderWikiModule\__construct
__construct(array $options=null)
Definition: ResourceLoaderWikiModule.php:91
ResourceLoaderModule\validateScriptFile
validateScriptFile( $fileName, $contents)
Validate a user-provided JavaScript blob.
Definition: ResourceLoaderModule.php:1001
Content
Base interface for content objects.
Definition: Content.php:35
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:39
ResourceLoaderModule\$config
Config $config
Definition: ResourceLoaderModule.php:41
ResourceLoaderWikiModule\getContentObj
getContentObj(PageIdentity $page, ResourceLoaderContext $context, $maxRedirects=null)
Definition: ResourceLoaderWikiModule.php:210
$cache
$cache
Definition: mcc.php:33
ResourceLoaderWikiModule\shouldEmbedModule
shouldEmbedModule(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:258
CONTENT_MODEL_JAVASCRIPT
const CONTENT_MODEL_JAVASCRIPT
Definition: Defines.php:209
ResourceLoaderWikiModule\getContent
getContent( $titleText, ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:178
ResourceLoaderWikiModule\getDefinitionSummary
getDefinitionSummary(ResourceLoaderContext $context)
Definition: ResourceLoaderWikiModule.php:350
ResourceLoaderWikiModule\makeTitleKey
static makeTitleKey(LinkTarget $title)
Definition: ResourceLoaderWikiModule.php:392
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:407
ResourceLoaderWikiModule\getPages
getPages(ResourceLoaderContext $context)
Subclasses should return an associative array of resources in the module.
Definition: ResourceLoaderWikiModule.php:125
ResourceLoaderWikiModule\$group
string null $group
Group of module.
Definition: ResourceLoaderWikiModule.php:85
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
CONTENT_MODEL_CSS
const CONTENT_MODEL_CSS
Definition: Defines.php:210
CONTENT_FORMAT_JAVASCRIPT
const CONTENT_FORMAT_JAVASCRIPT
For JS pages.
Definition: Defines.php:226
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:234
ResourceLoaderWikiModule\getTitleInfo
getTitleInfo(ResourceLoaderContext $context)
Get the information about the wiki pages for a given context.
Definition: ResourceLoaderWikiModule.php:402
MediaWiki\Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:180
MediaWiki\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:40