MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
31 
38 class ResourceLoader implements LoggerAwareInterface {
40  const CACHE_VERSION = 8;
41 
43  protected static $debugMode = null;
44 
49  protected $modules = [];
50 
55  protected $moduleInfos = [];
56 
58  protected $config;
59 
65  protected $testModuleNames = [];
66 
71  protected $sources = [];
72 
77  protected $errors = [];
78 
86  protected $extraHeaders = [];
87 
91  protected $blobStore;
92 
96  private $logger;
97 
99  const FILTER_NOMIN = '/*@nomin*/';
100 
115  public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
116  if ( !$moduleNames ) {
117  // Or else Database*::select() will explode, plus it's cheaper!
118  return;
119  }
120  $dbr = wfGetDB( DB_REPLICA );
121  $skin = $context->getSkin();
122  $lang = $context->getLanguage();
123 
124  // Batched version of ResourceLoaderModule::getFileDependencies
125  $vary = "$skin|$lang";
126  $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [
127  'md_module' => $moduleNames,
128  'md_skin' => $vary,
129  ], __METHOD__
130  );
131 
132  // Prime in-object cache for file dependencies
133  $modulesWithDeps = [];
134  foreach ( $res as $row ) {
135  $module = $this->getModule( $row->md_module );
136  if ( $module ) {
137  $module->setFileDependencies( $context, ResourceLoaderModule::expandRelativePaths(
138  json_decode( $row->md_deps, true )
139  ) );
140  $modulesWithDeps[] = $row->md_module;
141  }
142  }
143  // Register the absence of a dependency row too
144  foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) {
145  $module = $this->getModule( $name );
146  if ( $module ) {
147  $this->getModule( $name )->setFileDependencies( $context, [] );
148  }
149  }
150 
151  // Batched version of ResourceLoaderWikiModule::getTitleInfo
152  ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
153 
154  // Prime in-object cache for message blobs for modules with messages
155  $modules = [];
156  foreach ( $moduleNames as $name ) {
157  $module = $this->getModule( $name );
158  if ( $module && $module->getMessages() ) {
159  $modules[$name] = $module;
160  }
161  }
162  $store = $this->getMessageBlobStore();
163  $blobs = $store->getBlobs( $modules, $lang );
164  foreach ( $blobs as $name => $blob ) {
165  $modules[$name]->setMessageBlob( $blob, $lang );
166  }
167  }
168 
186  public static function filter( $filter, $data, array $options = [] ) {
187  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
188  return $data;
189  }
190 
191  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
192  return self::applyFilter( $filter, $data );
193  }
194 
195  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
197 
198  $key = $cache->makeGlobalKey(
199  'resourceloader-filter',
200  $filter,
201  self::CACHE_VERSION,
202  md5( $data )
203  );
204 
205  $result = $cache->get( $key );
206  if ( $result === false ) {
207  $stats->increment( "resourceloader_cache.$filter.miss" );
208  $result = self::applyFilter( $filter, $data );
209  $cache->set( $key, $result, 24 * 3600 );
210  } else {
211  $stats->increment( "resourceloader_cache.$filter.hit" );
212  }
213  if ( $result === null ) {
214  // Cached failure
215  $result = $data;
216  }
217 
218  return $result;
219  }
220 
221  private static function applyFilter( $filter, $data ) {
222  $data = trim( $data );
223  if ( $data ) {
224  try {
225  $data = ( $filter === 'minify-css' )
226  ? CSSMin::minify( $data )
227  : JavaScriptMinifier::minify( $data );
228  } catch ( Exception $e ) {
230  return null;
231  }
232  }
233  return $data;
234  }
235 
241  public function __construct( Config $config = null, LoggerInterface $logger = null ) {
242  $this->logger = $logger ?: new NullLogger();
243 
244  if ( !$config ) {
245  wfDeprecated( __METHOD__ . ' without a Config instance', '1.34' );
246  $config = MediaWikiServices::getInstance()->getMainConfig();
247  }
248  $this->config = $config;
249 
250  // Add 'local' source first
251  $this->addSource( 'local', $config->get( 'LoadScript' ) );
252 
253  // Special module that always exists
254  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
255 
256  $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
257  }
258 
262  public function getConfig() {
263  return $this->config;
264  }
265 
270  public function setLogger( LoggerInterface $logger ) {
271  $this->logger = $logger;
272  }
273 
278  public function getLogger() {
279  return $this->logger;
280  }
281 
286  public function getMessageBlobStore() {
287  return $this->blobStore;
288  }
289 
295  $this->blobStore = $blobStore;
296  }
297 
309  public function register( $name, $info = null ) {
310  $moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
311 
312  // Allow multiple modules to be registered in one call
313  $registrations = is_array( $name ) ? $name : [ $name => $info ];
314  foreach ( $registrations as $name => $info ) {
315  // Warn on duplicate registrations
316  if ( isset( $this->moduleInfos[$name] ) ) {
317  // A module has already been registered by this name
318  $this->logger->warning(
319  'ResourceLoader duplicate registration warning. ' .
320  'Another module has already been registered as ' . $name
321  );
322  }
323 
324  // Check $name for validity
325  if ( !self::isValidModuleName( $name ) ) {
326  throw new MWException( "ResourceLoader module name '$name' is invalid, "
327  . "see ResourceLoader::isValidModuleName()" );
328  }
329 
330  // Attach module
331  if ( $info instanceof ResourceLoaderModule ) {
332  $this->moduleInfos[$name] = [ 'object' => $info ];
333  $info->setName( $name );
334  $this->modules[$name] = $info;
335  } elseif ( is_array( $info ) ) {
336  // New calling convention
337  $this->moduleInfos[$name] = $info;
338  } else {
339  throw new MWException(
340  'ResourceLoader module info type error for module \'' . $name .
341  '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
342  );
343  }
344 
345  // Last-minute changes
346 
347  // Apply custom skin-defined styles to existing modules.
348  if ( $this->isFileModule( $name ) ) {
349  foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
350  // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
351  if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
352  continue;
353  }
354 
355  // If $name is preceded with a '+', the defined style files will be added to 'default'
356  // skinStyles, otherwise 'default' will be ignored as it normally would be.
357  if ( isset( $skinStyles[$name] ) ) {
358  $paths = (array)$skinStyles[$name];
359  $styleFiles = [];
360  } elseif ( isset( $skinStyles['+' . $name] ) ) {
361  $paths = (array)$skinStyles['+' . $name];
362  $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
363  (array)$this->moduleInfos[$name]['skinStyles']['default'] :
364  [];
365  } else {
366  continue;
367  }
368 
369  // Add new file paths, remapping them to refer to our directories and not use settings
370  // from the module we're modifying, which come from the base definition.
371  list( $localBasePath, $remoteBasePath ) =
373 
374  foreach ( $paths as $path ) {
375  $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
376  }
377 
378  $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
379  }
380  }
381  }
382  }
383 
387  public function registerTestModules() {
388  global $IP;
389 
390  if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
391  throw new MWException( 'Attempt to register JavaScript test modules '
392  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
393  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
394  }
395 
396  $testModules = [
397  'qunit' => [],
398  ];
399 
400  // Get test suites from extensions
401  // Avoid PHP 7.1 warning from passing $this by reference
402  $rl = $this;
403  Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
404  $extRegistry = ExtensionRegistry::getInstance();
405  // In case of conflict, the deprecated hook has precedence.
406  $testModules['qunit'] += $extRegistry->getAttribute( 'QUnitTestModules' );
407 
408  // Add the QUnit testrunner as implicit dependency to extension test suites.
409  foreach ( $testModules['qunit'] as &$module ) {
410  // Shuck any single-module dependency as an array
411  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
412  $module['dependencies'] = [ $module['dependencies'] ];
413  }
414 
415  $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
416  }
417 
418  // Get core test suites
419  $testModules['qunit'] =
420  ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
421 
422  foreach ( $testModules as $id => $names ) {
423  // Register test modules
424  $this->register( $testModules[$id] );
425 
426  // Keep track of their names so that they can be loaded together
427  $this->testModuleNames[$id] = array_keys( $testModules[$id] );
428  }
429  }
430 
441  public function addSource( $id, $loadUrl = null ) {
442  // Allow multiple sources to be registered in one call
443  if ( is_array( $id ) ) {
444  foreach ( $id as $key => $value ) {
445  $this->addSource( $key, $value );
446  }
447  return;
448  }
449 
450  // Disallow duplicates
451  if ( isset( $this->sources[$id] ) ) {
452  throw new MWException(
453  'ResourceLoader duplicate source addition error. ' .
454  'Another source has already been registered as ' . $id
455  );
456  }
457 
458  // Pre 1.24 backwards-compatibility
459  if ( is_array( $loadUrl ) ) {
460  if ( !isset( $loadUrl['loadScript'] ) ) {
461  throw new MWException(
462  __METHOD__ . ' was passed an array with no "loadScript" key.'
463  );
464  }
465 
466  $loadUrl = $loadUrl['loadScript'];
467  }
468 
469  $this->sources[$id] = $loadUrl;
470  }
471 
477  public function getModuleNames() {
478  return array_keys( $this->moduleInfos );
479  }
480 
491  public function getTestModuleNames( $framework = 'all' ) {
493  if ( $framework == 'all' ) {
494  return $this->testModuleNames;
495  } elseif ( isset( $this->testModuleNames[$framework] )
496  && is_array( $this->testModuleNames[$framework] )
497  ) {
498  return $this->testModuleNames[$framework];
499  } else {
500  return [];
501  }
502  }
503 
511  public function isModuleRegistered( $name ) {
512  return isset( $this->moduleInfos[$name] );
513  }
514 
526  public function getModule( $name ) {
527  if ( !isset( $this->modules[$name] ) ) {
528  if ( !isset( $this->moduleInfos[$name] ) ) {
529  // No such module
530  return null;
531  }
532  // Construct the requested object
533  $info = $this->moduleInfos[$name];
535  if ( isset( $info['object'] ) ) {
536  // Object given in info array
537  $object = $info['object'];
538  } elseif ( isset( $info['factory'] ) ) {
539  $object = call_user_func( $info['factory'], $info );
540  $object->setConfig( $this->getConfig() );
541  $object->setLogger( $this->logger );
542  } else {
543  $class = $info['class'] ?? ResourceLoaderFileModule::class;
545  $object = new $class( $info );
546  $object->setConfig( $this->getConfig() );
547  $object->setLogger( $this->logger );
548  }
549  $object->setName( $name );
550  $this->modules[$name] = $object;
551  }
552 
553  return $this->modules[$name];
554  }
555 
562  protected function isFileModule( $name ) {
563  if ( !isset( $this->moduleInfos[$name] ) ) {
564  return false;
565  }
566  $info = $this->moduleInfos[$name];
567  if ( isset( $info['object'] ) ) {
568  return false;
569  }
570  return (
571  // The implied default for 'class' is ResourceLoaderFileModule
572  !isset( $info['class'] ) ||
573  // Explicit default
574  $info['class'] === ResourceLoaderFileModule::class ||
575  is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
576  );
577  }
578 
584  public function getSources() {
585  return $this->sources;
586  }
587 
597  public function getLoadScript( $source ) {
598  if ( !isset( $this->sources[$source] ) ) {
599  throw new MWException( "The $source source was never registered in ResourceLoader." );
600  }
601  return $this->sources[$source];
602  }
603 
609  public static function makeHash( $value ) {
610  $hash = hash( 'fnv132', $value );
611  return Wikimedia\base_convert( $hash, 16, 36, 7 );
612  }
613 
623  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
625  $this->logger->warning(
626  $msg,
627  $context + [ 'exception' => $e ]
628  );
629  $this->errors[] = self::formatExceptionNoComment( $e );
630  }
631 
640  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
641  if ( !$moduleNames ) {
642  return '';
643  }
644  $hashes = array_map( function ( $module ) use ( $context ) {
645  try {
646  return $this->getModule( $module )->getVersionHash( $context );
647  } catch ( Exception $e ) {
648  // If modules fail to compute a version, don't fail the request (T152266)
649  // and still compute versions of other modules.
650  $this->outputErrorAndLog( $e,
651  'Calculating version for "{module}" failed: {exception}',
652  [
653  'module' => $module,
654  ]
655  );
656  return '';
657  }
658  }, $moduleNames );
659  return self::makeHash( implode( '', $hashes ) );
660  }
661 
675  public function makeVersionQuery( ResourceLoaderContext $context ) {
676  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
677  // version hashes. There is no technical reason for this to be same, and for years the
678  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
679  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
680  // query parameter), then this method must continue to match the JS one.
681  $moduleNames = [];
682  foreach ( $context->getModules() as $name ) {
683  if ( !$this->getModule( $name ) ) {
684  // If a versioned request contains a missing module, the version is a mismatch
685  // as the client considered a module (and version) we don't have.
686  return '';
687  }
688  $moduleNames[] = $name;
689  }
690  return $this->getCombinedVersion( $context, $moduleNames );
691  }
692 
698  public function respond( ResourceLoaderContext $context ) {
699  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
700  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
701  // is used: ob_clean() will clear the GZIP header in that case and it won't come
702  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
703  // the whole thing in our own output buffer to be sure the active buffer
704  // doesn't use ob_gzhandler.
705  // See https://bugs.php.net/bug.php?id=36514
706  ob_start();
707 
708  $this->measureResponseTime( RequestContext::getMain()->getTiming() );
709 
710  // Find out which modules are missing and instantiate the others
711  $modules = [];
712  $missing = [];
713  foreach ( $context->getModules() as $name ) {
714  $module = $this->getModule( $name );
715  if ( $module ) {
716  // Do not allow private modules to be loaded from the web.
717  // This is a security issue, see T36907.
718  if ( $module->getGroup() === 'private' ) {
719  $this->logger->debug( "Request for private module '$name' denied" );
720  $this->errors[] = "Cannot show private module \"$name\"";
721  continue;
722  }
723  $modules[$name] = $module;
724  } else {
725  $missing[] = $name;
726  }
727  }
728 
729  try {
730  // Preload for getCombinedVersion() and for batch makeModuleResponse()
731  $this->preloadModuleInfo( array_keys( $modules ), $context );
732  } catch ( Exception $e ) {
733  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
734  }
735 
736  // Combine versions to propagate cache invalidation
737  $versionHash = '';
738  try {
739  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
740  } catch ( Exception $e ) {
741  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
742  }
743 
744  // See RFC 2616 § 3.11 Entity Tags
745  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
746  $etag = 'W/"' . $versionHash . '"';
747 
748  // Try the client-side cache first
749  if ( $this->tryRespondNotModified( $context, $etag ) ) {
750  return; // output handled (buffers cleared)
751  }
752 
753  // Use file cache if enabled and available...
754  if ( $this->config->get( 'UseFileCache' ) ) {
755  $fileCache = ResourceFileCache::newFromContext( $context );
756  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
757  return; // output handled
758  }
759  }
760 
761  // Generate a response
762  $response = $this->makeModuleResponse( $context, $modules, $missing );
763 
764  // Capture any PHP warnings from the output buffer and append them to the
765  // error list if we're in debug mode.
766  if ( $context->getDebug() ) {
767  $warnings = ob_get_contents();
768  if ( strlen( $warnings ) ) {
769  $this->errors[] = $warnings;
770  }
771  }
772 
773  // Save response to file cache unless there are errors
774  if ( isset( $fileCache ) && !$this->errors && $missing === [] ) {
775  // Cache single modules and images...and other requests if there are enough hits
776  if ( ResourceFileCache::useFileCache( $context ) ) {
777  if ( $fileCache->isCacheWorthy() ) {
778  $fileCache->saveText( $response );
779  } else {
780  $fileCache->incrMissesRecent( $context->getRequest() );
781  }
782  }
783  }
784 
785  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
786 
787  // Remove the output buffer and output the response
788  ob_end_clean();
789 
790  if ( $context->getImageObj() && $this->errors ) {
791  // We can't show both the error messages and the response when it's an image.
792  $response = implode( "\n\n", $this->errors );
793  } elseif ( $this->errors ) {
794  $errorText = implode( "\n\n", $this->errors );
795  $errorResponse = self::makeComment( $errorText );
796  if ( $context->shouldIncludeScripts() ) {
797  $errorResponse .= 'if (window.console && console.error) {'
798  . Xml::encodeJsCall( 'console.error', [ $errorText ] )
799  . "}\n";
800  }
801 
802  // Prepend error info to the response
803  $response = $errorResponse . $response;
804  }
805 
806  $this->errors = [];
807  echo $response;
808  }
809 
810  protected function measureResponseTime( Timing $timing ) {
811  DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
812  $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
813  if ( $measure !== false ) {
814  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
815  $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
816  }
817  } );
818  }
819 
831  protected function sendResponseHeaders(
832  ResourceLoaderContext $context, $etag, $errors, array $extra = []
833  ) {
835  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
836  // Use a short cache expiry so that updates propagate to clients quickly, if:
837  // - No version specified (shared resources, e.g. stylesheets)
838  // - There were errors (recover quickly)
839  // - Version mismatch (T117587, T47877)
840  if ( is_null( $context->getVersion() )
841  || $errors
842  || $context->getVersion() !== $this->makeVersionQuery( $context )
843  ) {
844  $maxage = $rlMaxage['unversioned']['client'];
845  $smaxage = $rlMaxage['unversioned']['server'];
846  // If a version was specified we can use a longer expiry time since changing
847  // version numbers causes cache misses
848  } else {
849  $maxage = $rlMaxage['versioned']['client'];
850  $smaxage = $rlMaxage['versioned']['server'];
851  }
852  if ( $context->getImageObj() ) {
853  // Output different headers if we're outputting textual errors.
854  if ( $errors ) {
855  header( 'Content-Type: text/plain; charset=utf-8' );
856  } else {
857  $context->getImageObj()->sendResponseHeaders( $context );
858  }
859  } elseif ( $context->getOnly() === 'styles' ) {
860  header( 'Content-Type: text/css; charset=utf-8' );
861  header( 'Access-Control-Allow-Origin: *' );
862  } else {
863  header( 'Content-Type: text/javascript; charset=utf-8' );
864  }
865  // See RFC 2616 § 14.19 ETag
866  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
867  header( 'ETag: ' . $etag );
868  if ( $context->getDebug() ) {
869  // Do not cache debug responses
870  header( 'Cache-Control: private, no-cache, must-revalidate' );
871  header( 'Pragma: no-cache' );
872  } else {
873  header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
874  $exp = min( $maxage, $smaxage );
875  header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
876  }
877  foreach ( $extra as $header ) {
878  header( $header );
879  }
880  }
881 
892  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
893  // See RFC 2616 § 14.26 If-None-Match
894  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
895  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
896  // Never send 304s in debug mode
897  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
898  // There's another bug in ob_gzhandler (see also the comment at
899  // the top of this function) that causes it to gzip even empty
900  // responses, meaning it's impossible to produce a truly empty
901  // response (because the gzip header is always there). This is
902  // a problem because 304 responses have to be completely empty
903  // per the HTTP spec, and Firefox behaves buggily when they're not.
904  // See also https://bugs.php.net/bug.php?id=51579
905  // To work around this, we tear down all output buffering before
906  // sending the 304.
907  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
908 
909  HttpStatus::header( 304 );
910 
911  $this->sendResponseHeaders( $context, $etag, false );
912  return true;
913  }
914  return false;
915  }
916 
925  protected function tryRespondFromFileCache(
926  ResourceFileCache $fileCache,
927  ResourceLoaderContext $context,
928  $etag
929  ) {
930  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
931  // Buffer output to catch warnings.
932  ob_start();
933  // Get the maximum age the cache can be
934  $maxage = is_null( $context->getVersion() )
935  ? $rlMaxage['unversioned']['server']
936  : $rlMaxage['versioned']['server'];
937  // Minimum timestamp the cache file must have
938  $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
939  if ( !$good ) {
940  try { // RL always hits the DB on file cache miss...
941  wfGetDB( DB_REPLICA );
942  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
943  $good = $fileCache->isCacheGood(); // cache existence check
944  }
945  }
946  if ( $good ) {
947  $ts = $fileCache->cacheTimestamp();
948  // Send content type and cache headers
949  $this->sendResponseHeaders( $context, $etag, false );
950  $response = $fileCache->fetchText();
951  // Capture any PHP warnings from the output buffer and append them to the
952  // response in a comment if we're in debug mode.
953  if ( $context->getDebug() ) {
954  $warnings = ob_get_contents();
955  if ( strlen( $warnings ) ) {
956  $response = self::makeComment( $warnings ) . $response;
957  }
958  }
959  // Remove the output buffer and output the response
960  ob_end_clean();
961  echo $response . "\n/* Cached {$ts} */";
962  return true; // cache hit
963  }
964  // Clear buffer
965  ob_end_clean();
966 
967  return false; // cache miss
968  }
969 
978  public static function makeComment( $text ) {
979  $encText = str_replace( '*/', '* /', $text );
980  return "/*\n$encText\n*/\n";
981  }
982 
989  public static function formatException( $e ) {
990  return self::makeComment( self::formatExceptionNoComment( $e ) );
991  }
992 
1000  protected static function formatExceptionNoComment( $e ) {
1001  global $wgShowExceptionDetails;
1002 
1003  if ( !$wgShowExceptionDetails ) {
1005  }
1006 
1008  "\nBacktrace:\n" .
1010  }
1011 
1023  public function makeModuleResponse( ResourceLoaderContext $context,
1024  array $modules, array $missing = []
1025  ) {
1026  $out = '';
1027  $states = [];
1028 
1029  if ( $modules === [] && $missing === [] ) {
1030  return <<<MESSAGE
1031 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1032  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1033  no modules were requested. Max made me put this here. */
1034 MESSAGE;
1035  }
1036 
1037  $image = $context->getImageObj();
1038  if ( $image ) {
1039  $data = $image->getImageData( $context );
1040  if ( $data === false ) {
1041  $data = '';
1042  $this->errors[] = 'Image generation failed';
1043  }
1044  return $data;
1045  }
1046 
1047  foreach ( $missing as $name ) {
1048  $states[$name] = 'missing';
1049  }
1050 
1051  // Generate output
1052  $isRaw = false;
1053 
1054  $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1055 
1056  foreach ( $modules as $name => $module ) {
1057  try {
1058  $content = $module->getModuleContent( $context );
1059  $implementKey = $name . '@' . $module->getVersionHash( $context );
1060  $strContent = '';
1061 
1062  if ( isset( $content['headers'] ) ) {
1063  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1064  }
1065 
1066  // Append output
1067  switch ( $context->getOnly() ) {
1068  case 'scripts':
1069  $scripts = $content['scripts'];
1070  if ( is_string( $scripts ) ) {
1071  // Load scripts raw...
1072  $strContent = $scripts;
1073  } elseif ( is_array( $scripts ) ) {
1074  // ...except when $scripts is an array of URLs or an associative array
1075  $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
1076  }
1077  break;
1078  case 'styles':
1079  $styles = $content['styles'];
1080  // We no longer separate into media, they are all combined now with
1081  // custom media type groups into @media .. {} sections as part of the css string.
1082  // Module returns either an empty array or a numerical array with css strings.
1083  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1084  break;
1085  default:
1086  $scripts = $content['scripts'] ?? '';
1087  if ( is_string( $scripts ) ) {
1088  if ( $name === 'site' || $name === 'user' ) {
1089  // Legacy scripts that run in the global scope without a closure.
1090  // mw.loader.implement will use globalEval if scripts is a string.
1091  // Minify manually here, because general response minification is
1092  // not effective due it being a string literal, not a function.
1093  if ( !$context->getDebug() ) {
1094  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1095  }
1096  } else {
1097  $scripts = new XmlJsCode( $scripts );
1098  }
1099  }
1100  $strContent = self::makeLoaderImplementScript(
1101  $implementKey,
1102  $scripts,
1103  $content['styles'] ?? [],
1104  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1105  $content['templates'] ?? []
1106  );
1107  break;
1108  }
1109 
1110  if ( !$context->getDebug() ) {
1111  $strContent = self::filter( $filter, $strContent );
1112  } else {
1113  // In debug mode, separate each response by a new line.
1114  // For example, between 'mw.loader.implement();' statements.
1115  $strContent = $this->ensureNewline( $strContent );
1116  }
1117 
1118  if ( $context->getOnly() === 'scripts' ) {
1119  // Use a linebreak between module scripts (T162719)
1120  $out .= $this->ensureNewline( $strContent );
1121  } else {
1122  $out .= $strContent;
1123  }
1124 
1125  } catch ( Exception $e ) {
1126  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1127 
1128  // Respond to client with error-state instead of module implementation
1129  $states[$name] = 'error';
1130  unset( $modules[$name] );
1131  }
1132  $isRaw |= $module->isRaw();
1133  }
1134 
1135  // Update module states
1136  if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
1137  if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
1138  // Set the state of modules loaded as only scripts to ready as
1139  // they don't have an mw.loader.implement wrapper that sets the state
1140  foreach ( $modules as $name => $module ) {
1141  $states[$name] = 'ready';
1142  }
1143  }
1144 
1145  // Set the state of modules we didn't respond to with mw.loader.implement
1146  if ( count( $states ) ) {
1147  $stateScript = self::makeLoaderStateScript( $states );
1148  if ( !$context->getDebug() ) {
1149  $stateScript = self::filter( 'minify-js', $stateScript );
1150  }
1151  // Use a linebreak between module script and state script (T162719)
1152  $out = $this->ensureNewline( $out ) . $stateScript;
1153  }
1154  } elseif ( $states ) {
1155  $this->errors[] = 'Problematic modules: '
1156  . self::encodeJsonForScript( $states );
1157  }
1158 
1159  return $out;
1160  }
1161 
1167  private function ensureNewline( $str ) {
1168  $end = substr( $str, -1 );
1169  if ( $end === false || $end === '' || $end === "\n" ) {
1170  return $str;
1171  }
1172  return $str . "\n";
1173  }
1174 
1181  public function getModulesByMessage( $messageKey ) {
1182  $moduleNames = [];
1183  foreach ( $this->getModuleNames() as $moduleName ) {
1184  $module = $this->getModule( $moduleName );
1185  if ( in_array( $messageKey, $module->getMessages() ) ) {
1186  $moduleNames[] = $moduleName;
1187  }
1188  }
1189  return $moduleNames;
1190  }
1191 
1209  protected static function makeLoaderImplementScript(
1210  $name, $scripts, $styles, $messages, $templates
1211  ) {
1212  if ( $scripts instanceof XmlJsCode ) {
1213  if ( $scripts->value === '' ) {
1214  $scripts = null;
1215  } elseif ( self::inDebugMode() ) {
1216  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1217  } else {
1218  $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
1219  }
1220  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1221  $files = $scripts['files'];
1222  foreach ( $files as $path => &$file ) {
1223  // $file is changed (by reference) from a descriptor array to the content of the file
1224  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1225  if ( $file['type'] === 'script' ) {
1226  // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1227  if ( self::inDebugMode() ) {
1228  $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
1229  } else {
1230  $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
1231  }
1232  } else {
1233  $file = $file['content'];
1234  }
1235  }
1236  $scripts = XmlJsCode::encodeObject( [
1237  'main' => $scripts['main'],
1238  'files' => XmlJsCode::encodeObject( $files, self::inDebugMode() )
1239  ], self::inDebugMode() );
1240  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1241  throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
1242  }
1243 
1244  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1245  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1246  // of "{}". Force them to objects.
1247  $module = [
1248  $name,
1249  $scripts,
1250  (object)$styles,
1251  (object)$messages,
1252  (object)$templates
1253  ];
1254  self::trimArray( $module );
1255 
1256  return Xml::encodeJsCall( 'mw.loader.implement', $module, self::inDebugMode() );
1257  }
1258 
1266  public static function makeMessageSetScript( $messages ) {
1267  return Xml::encodeJsCall(
1268  'mw.messages.set',
1269  [ (object)$messages ],
1270  self::inDebugMode()
1271  );
1272  }
1273 
1281  public static function makeCombinedStyles( array $stylePairs ) {
1282  $out = [];
1283  foreach ( $stylePairs as $media => $styles ) {
1284  // ResourceLoaderFileModule::getStyle can return the styles
1285  // as a string or an array of strings. This is to allow separation in
1286  // the front-end.
1287  $styles = (array)$styles;
1288  foreach ( $styles as $style ) {
1289  $style = trim( $style );
1290  // Don't output an empty "@media print { }" block (T42498)
1291  if ( $style !== '' ) {
1292  // Transform the media type based on request params and config
1293  // The way that this relies on $wgRequest to propagate request params is slightly evil
1294  $media = OutputPage::transformCssMedia( $media );
1295 
1296  if ( $media === '' || $media == 'all' ) {
1297  $out[] = $style;
1298  } elseif ( is_string( $media ) ) {
1299  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1300  }
1301  // else: skip
1302  }
1303  }
1304  }
1305  return $out;
1306  }
1307 
1317  public static function encodeJsonForScript( $data ) {
1318  // Keep output as small as possible by disabling needless escape modes
1319  // that PHP uses by default.
1320  // However, while most module scripts are only served on HTTP responses
1321  // for JavaScript, some modules can also be embedded in the HTML as inline
1322  // scripts. This, and the fact that we sometimes need to export strings
1323  // containing user-generated content and labels that may genuinely contain
1324  // a sequences like "</script>", we need to encode either '/' or '<'.
1325  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1326  // and allows URLs to mostly remain readable.
1327  $jsonFlags = JSON_UNESCAPED_SLASHES |
1328  JSON_UNESCAPED_UNICODE |
1329  JSON_HEX_TAG |
1330  JSON_HEX_AMP;
1331  if ( self::inDebugMode() ) {
1332  $jsonFlags |= JSON_PRETTY_PRINT;
1333  }
1334  return json_encode( $data, $jsonFlags );
1335  }
1336 
1351  public static function makeLoaderStateScript( $states, $state = null ) {
1352  if ( !is_array( $states ) ) {
1353  $states = [ $states => $state ];
1354  }
1355  return Xml::encodeJsCall(
1356  'mw.loader.state',
1357  [ $states ],
1358  self::inDebugMode()
1359  );
1360  }
1361 
1362  private static function isEmptyObject( stdClass $obj ) {
1363  foreach ( $obj as $key => $value ) {
1364  return false;
1365  }
1366  return true;
1367  }
1368 
1381  private static function trimArray( array &$array ) {
1382  $i = count( $array );
1383  while ( $i-- ) {
1384  if ( $array[$i] === null
1385  || $array[$i] === []
1386  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1387  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1388  ) {
1389  unset( $array[$i] );
1390  } else {
1391  break;
1392  }
1393  }
1394  }
1395 
1421  public static function makeLoaderRegisterScript( array $modules ) {
1422  // Optimisation: Transform dependency names into indexes when possible
1423  // to produce smaller output. They are expanded by mw.loader.register on
1424  // the other end using resolveIndexedDependencies().
1425  $index = [];
1426  foreach ( $modules as $i => &$module ) {
1427  // Build module name index
1428  $index[$module[0]] = $i;
1429  }
1430  foreach ( $modules as &$module ) {
1431  if ( isset( $module[2] ) ) {
1432  foreach ( $module[2] as &$dependency ) {
1433  if ( isset( $index[$dependency] ) ) {
1434  // Replace module name in dependency list with index
1435  $dependency = $index[$dependency];
1436  }
1437  }
1438  }
1439  }
1440 
1441  array_walk( $modules, [ self::class, 'trimArray' ] );
1442 
1443  return Xml::encodeJsCall(
1444  'mw.loader.register',
1445  [ $modules ],
1446  self::inDebugMode()
1447  );
1448  }
1449 
1464  public static function makeLoaderSourcesScript( $sources, $loadUrl = null ) {
1465  if ( !is_array( $sources ) ) {
1466  $sources = [ $sources => $loadUrl ];
1467  }
1468  return Xml::encodeJsCall(
1469  'mw.loader.addSource',
1470  [ $sources ],
1471  self::inDebugMode()
1472  );
1473  }
1474 
1481  public static function makeLoaderConditionalScript( $script ) {
1482  // Adds a function to lazy-created RLQ
1483  return '(RLQ=window.RLQ||[]).push(function(){' .
1484  trim( $script ) . '});';
1485  }
1486 
1495  public static function makeInlineCodeWithModule( $modules, $script ) {
1496  // Adds an array to lazy-created RLQ
1497  return '(RLQ=window.RLQ||[]).push(['
1498  . self::encodeJsonForScript( $modules ) . ','
1499  . 'function(){' . trim( $script ) . '}'
1500  . ']);';
1501  }
1502 
1514  public static function makeInlineScript( $script, $nonce = null ) {
1515  $js = self::makeLoaderConditionalScript( $script );
1516  $escNonce = '';
1517  if ( $nonce === null ) {
1518  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1519  } elseif ( $nonce !== false ) {
1520  // If it was false, CSP is disabled, so no nonce attribute.
1521  // Nonce should be only base64 characters, so should be safe,
1522  // but better to be safely escaped than sorry.
1523  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1524  }
1525 
1526  return new WrappedString(
1527  Html::inlineScript( $js, $nonce ),
1528  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1529  '});</script>'
1530  );
1531  }
1532 
1541  public static function makeConfigSetScript( array $configuration ) {
1542  $js = Xml::encodeJsCall(
1543  'mw.config.set',
1544  [ $configuration ],
1545  self::inDebugMode()
1546  );
1547  if ( $js === false ) {
1548  $e = new Exception(
1549  'JSON serialization of config data failed. ' .
1550  'This usually means the config data is not valid UTF-8.'
1551  );
1553  $js = Xml::encodeJsCall( 'mw.log.error', [ $e->__toString() ] );
1554  }
1555  return $js;
1556  }
1557 
1571  public static function makePackedModulesString( $modules ) {
1572  $moduleMap = []; // [ prefix => [ suffixes ] ]
1573  foreach ( $modules as $module ) {
1574  $pos = strrpos( $module, '.' );
1575  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1576  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1577  $moduleMap[$prefix][] = $suffix;
1578  }
1579 
1580  $arr = [];
1581  foreach ( $moduleMap as $prefix => $suffixes ) {
1582  $p = $prefix === '' ? '' : $prefix . '.';
1583  $arr[] = $p . implode( ',', $suffixes );
1584  }
1585  return implode( '|', $arr );
1586  }
1587 
1599  public static function expandModuleNames( $modules ) {
1600  $retval = [];
1601  $exploded = explode( '|', $modules );
1602  foreach ( $exploded as $group ) {
1603  if ( strpos( $group, ',' ) === false ) {
1604  // This is not a set of modules in foo.bar,baz notation
1605  // but a single module
1606  $retval[] = $group;
1607  } else {
1608  // This is a set of modules in foo.bar,baz notation
1609  $pos = strrpos( $group, '.' );
1610  if ( $pos === false ) {
1611  // Prefixless modules, i.e. without dots
1612  $retval = array_merge( $retval, explode( ',', $group ) );
1613  } else {
1614  // We have a prefix and a bunch of suffixes
1615  $prefix = substr( $group, 0, $pos ); // 'foo'
1616  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1617  foreach ( $suffixes as $suffix ) {
1618  $retval[] = "$prefix.$suffix";
1619  }
1620  }
1621  }
1622  }
1623  return $retval;
1624  }
1625 
1631  public static function inDebugMode() {
1632  if ( self::$debugMode === null ) {
1634  self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
1635  $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
1636  );
1637  }
1638  return self::$debugMode;
1639  }
1640 
1651  public static function clearCache() {
1652  self::$debugMode = null;
1653  }
1654 
1664  public function createLoaderURL( $source, ResourceLoaderContext $context,
1665  $extraQuery = []
1666  ) {
1667  $query = self::createLoaderQuery( $context, $extraQuery );
1668  $script = $this->getLoadScript( $source );
1669 
1670  return wfAppendQuery( $script, $query );
1671  }
1672 
1682  protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
1683  return self::makeLoaderQuery(
1684  $context->getModules(),
1685  $context->getLanguage(),
1686  $context->getSkin(),
1687  $context->getUser(),
1688  $context->getVersion(),
1689  $context->getDebug(),
1690  $context->getOnly(),
1691  $context->getRequest()->getBool( 'printable' ),
1692  $context->getRequest()->getBool( 'handheld' ),
1693  $extraQuery
1694  );
1695  }
1696 
1713  public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
1714  $version = null, $debug = false, $only = null, $printable = false,
1715  $handheld = false, $extraQuery = []
1716  ) {
1717  $query = [
1718  'modules' => self::makePackedModulesString( $modules ),
1719  ];
1720  // Keep urls short by omitting query parameters that
1721  // match the defaults assumed by ResourceLoaderContext.
1722  // Note: This relies on the defaults either being insignificant or forever constant,
1723  // as otherwise cached urls could change in meaning when the defaults change.
1724  if ( $lang !== 'qqx' ) {
1725  $query['lang'] = $lang;
1726  }
1727  if ( $skin !== 'fallback' ) {
1728  $query['skin'] = $skin;
1729  }
1730  if ( $debug === true ) {
1731  $query['debug'] = 'true';
1732  }
1733  if ( $user !== null ) {
1734  $query['user'] = $user;
1735  }
1736  if ( $version !== null ) {
1737  $query['version'] = $version;
1738  }
1739  if ( $only !== null ) {
1740  $query['only'] = $only;
1741  }
1742  if ( $printable ) {
1743  $query['printable'] = 1;
1744  }
1745  if ( $handheld ) {
1746  $query['handheld'] = 1;
1747  }
1748  $query += $extraQuery;
1749 
1750  // Make queries uniform in order
1751  ksort( $query );
1752  return $query;
1753  }
1754 
1764  public static function isValidModuleName( $moduleName ) {
1765  return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName );
1766  }
1767 
1778  public function getLessCompiler( $vars = [] ) {
1779  global $IP;
1780  // When called from the installer, it is possible that a required PHP extension
1781  // is missing (at least for now; see T49564). If this is the case, throw an
1782  // exception (caught by the installer) to prevent a fatal error later on.
1783  if ( !class_exists( 'Less_Parser' ) ) {
1784  throw new MWException( 'MediaWiki requires the less.php parser' );
1785  }
1786 
1787  $parser = new Less_Parser;
1788  $parser->ModifyVars( $vars );
1789  $parser->SetImportDirs( [
1790  "$IP/resources/src/mediawiki.less/" => '',
1791  ] );
1792  $parser->SetOption( 'relativeUrls', false );
1793 
1794  return $parser;
1795  }
1796 
1804  public function getLessVars() {
1805  return [];
1806  }
1807 }
getTestModuleNames( $framework='all')
Get a list of test module names for one (or all) frameworks.
static getLogMessage( $e)
Get a message formatting the exception message and its origin.
This class generates message blobs for use by ResourceLoader modules.
getModuleNames()
Get a list of module names.
isCacheGood( $timestamp='')
Check if up to date cache file exists.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
array $modules
Module name/ResourceLoaderModule object pairs.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1585
static filter( $filter, $data, array $options=[])
Run JavaScript or CSS data through a filter, caching the filtered result for future calls...
getSources()
Get the list of sources.
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
$IP
Definition: WebStart.php:41
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2159
createLoaderURL( $source, ResourceLoaderContext $context, $extraQuery=[])
Build a load.php URL.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation and configuration files Object form shall mean any form resulting from mechanical transformation or translation of a Source including but not limited to compiled object generated and conversions to other media types Work shall mean the work of whether in Source or Object made available under the as indicated by a copyright notice that is included in or attached to the whether in Source or Object that is based or other modifications as a an original work of authorship For the purposes of this Derivative Works shall not include works that remain separable from
if(!isset( $args[0])) $lang
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition: globals.txt:25
array $testModuleNames
Associative array mapping framework ids to a list of names of test suite modules like [ &#39;qunit&#39; => [ ...
isModuleRegistered( $name)
Check whether a ResourceLoader module is registered.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$source
$value
static minify( $css)
Removes whitespace from CSS data.
Definition: CSSMin.php:540
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
static makeConfigSetScript(array $configuration)
Returns JS code which will set the MediaWiki configuration array to the given value.
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:48
static newFromContext(ResourceLoaderContext $context)
Construct an ResourceFileCache from a context.
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
this hook is for auditing only $response
Definition: hooks.txt:780
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:573
static makeCombinedStyles(array $stylePairs)
Combines an associative array mapping media type to CSS into a single stylesheet with "@media" blocks...
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition: hooks.txt:1799
cacheTimestamp()
Get the last-modified timestamp of the cache file.
static makeMessageSetScript( $messages)
Returns JS code which, when called, will register a given list of messages.
static formatException( $e)
Handle exception display.
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropiate.
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1980
getCombinedVersion(ResourceLoaderContext $context, array $moduleNames)
Helper method to get and combine versions of multiple modules.
static formatExceptionNoComment( $e)
Handle exception display.
__construct(Config $config=null, LoggerInterface $logger=null)
Register core modules and runs registration hooks.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:780
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1982
preloadModuleInfo(array $moduleNames, ResourceLoaderContext $context)
Load information stored in the database about modules.
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
static encodeObject( $obj, $pretty=false)
Encode an object containing XmlJsCode objects.
Definition: XmlJsCode.php:58
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
MessageBlobStore $blobStore
static makeInlineCodeWithModule( $modules, $script)
Wraps JavaScript code to run after a required module.
static getMain()
Get the RequestContext object associated with the main request.
static getRedactedTraceAsString( $e)
Generate a string representation of an exception&#39;s stack trace.
static makeHash( $value)
Interface for configuration instances.
Definition: Config.php:28
getLoadScript( $source)
Get the URL to the load.php endpoint for the given ResourceLoader source.
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:677
ResourceLoader request result caching in the file system.
array $moduleInfos
Associative array mapping module name to info associative array.
array $extraHeaders
List of extra HTTP response headers provided by loaded modules.
$res
Definition: database.txt:21
setMessageBlobStore(MessageBlobStore $blobStore)
static warnIfHeadersSent()
Log a warning message if headers have already been sent.
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
makeVersionQuery(ResourceLoaderContext $context)
Get the expected value of the &#39;version&#39; query parameter.
A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to interpret a given string a...
Definition: XmlJsCode.php:40
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
$cache
Definition: mcc.php:33
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode...
static logException( $e, $catcher=self::CAUGHT_BY_OTHER)
Log an exception to the exception log (if enabled).
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1982
static makeLoaderStateScript( $states, $state=null)
Returns a JS call to mw.loader.state, which sets the state of one ore more modules to a given value...
An interface to help developers measure the performance of their applications.
Definition: Timing.php:45
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
the value of this variable comes from LanguageConverter indexed by page_id indexed by prefixed DB keys on which the links will be shown can modify can modify can modify this should be populated with an alert message to that effect to be fed to an HTMLForm object and populate $result with the reason in the form of [messagename, param1, param2,...] or a MessageSpecifier error messages should be plain text with no special etc to show that they re errors
Definition: hooks.txt:1746
$filter
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned $skin
Definition: hooks.txt:1982
$header
measureResponseTime(Timing $timing)
tryRespondFromFileCache(ResourceFileCache $fileCache, ResourceLoaderContext $context, $etag)
Send out code for a response from file cache if possible.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
fetchText()
Get the uncompressed text from the cache.
static makeInlineScript( $script, $nonce=null)
Returns an HTML script tag that runs given JS code after startup and base modules.
array $errors
Errors accumulated during current respond() call.
static createLoaderQuery(ResourceLoaderContext $context, $extraQuery=[])
Helper for createLoaderURL()
static isValidModuleName( $moduleName)
Check a module name for validity.
getLessCompiler( $vars=[])
Returns LESS compiler set up for use with MediaWiki.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path...
static clearCache()
Reset static members used for caching.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
static inDebugMode()
Determine whether debug mode was requested Order of priority is 1) request param, 2) cookie...
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
isFileModule( $name)
Whether the module is a ResourceLoaderFileModule (including subclasses).
sendResponseHeaders(ResourceLoaderContext $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
static bool $debugMode
static extractBasePaths( $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information. ...
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check $image
Definition: hooks.txt:780
static useFileCache(ResourceLoaderContext $context)
Check if an RL request can be cached.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
array $sources
E.g.
static makePackedModulesString( $modules)
Convert an array of module names to a packed query string.
static makeLoaderSourcesScript( $sources, $loadUrl=null)
Returns JS code which calls mw.loader.addSource() with the given parameters.
const CACHE_ANYTHING
Definition: Defines.php:97
setLogger(LoggerInterface $logger)
LoggerInterface $logger
static isEmptyObject(stdClass $obj)
static expandModuleNames( $modules)
Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to an array of module names like `[ &#39;jq...
outputErrorAndLog(Exception $e, $msg, array $context=[])
Add an error to the &#39;errors&#39; array and log it.
respond(ResourceLoaderContext $context)
Output a response to a load request, including the content-type header.
static makeLoaderRegisterScript(array $modules)
Returns JS code which calls mw.loader.register with the given parameter.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation and configuration files Object form shall mean any form resulting from mechanical transformation or translation of a Source including but not limited to compiled object generated and conversions to other media types Work shall mean the work of whether in Source or Object made available under the as indicated by a copyright notice that is included in or attached to the whether in Source or Object that is based or other modifications as a an original work of authorship For the purposes of this Derivative Works shall not include works that remain separable or merely the Work and Derivative Works thereof Contribution shall mean any work of including the original version of the Work and any modifications or additions to that Work or Derivative Works that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner For the purposes of this definition
addSource( $id, $loadUrl=null)
Add a foreign source of modules.
getLessVars()
Get global LESS variables.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
$messages
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:783
getImageObj()
If this is a request for an image, get the ResourceLoaderImage object.
$wgResourceLoaderDebug
The default debug mode (on/off) for of ResourceLoader requests.
static makeLoaderQuery( $modules, $lang, $skin, $user=null, $version=null, $debug=false, $only=null, $printable=false, $handheld=false, $extraQuery=[])
Build a query array (array representation of query string) for load.php.
const DB_REPLICA
Definition: defines.php:25
ensureNewline( $str)
Ensure the string is either empty or ends in a line break.
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
$debug
Definition: mcc.php:31
$content
Definition: pageupdater.txt:72
static makeLoaderConditionalScript( $script)
Wraps JavaScript code to run after the startup module.
measure( $measureName, $startMark='requestStart', $endMark=null)
This method stores the duration between two marks along with the associated name (a "measure")...
Definition: Timing.php:124
makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing=[])
Generate code for a response.
static getPublicLogMessage( $e)
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition: hooks.txt:2217
static applyFilter( $filter, $data)
static minify( $s)
Returns minified JavaScript code.
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
static makeLoaderImplementScript( $name, $scripts, $styles, $messages, $templates)
Return JS code that calls mw.loader.implement with given module properties.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static trimArray(array &$array)
Remove empty values from the end of an array.
static makeComment( $text)
Generate a CSS or JS comment block.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might include
Definition: hooks.txt:780
Object passed around to modules which contains information about the state of a specific loader reque...