MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
31 
38 class ResourceLoader implements LoggerAwareInterface {
40  protected $config;
42  protected $blobStore;
43 
45  private $logger;
46 
48  protected $modules = [];
50  protected $moduleInfos = [];
56  protected $testModuleNames = [];
58  protected $testSuiteModuleNames = [];
59 
61  protected $sources = [];
63  protected $errors = [];
65  protected $extraHeaders = [];
66 
68  protected static $debugMode = null;
69 
71  const CACHE_VERSION = 8;
72 
74  const FILTER_NOMIN = '/*@nomin*/';
75 
90  public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
91  if ( !$moduleNames ) {
92  // Or else Database*::select() will explode, plus it's cheaper!
93  return;
94  }
95  $dbr = wfGetDB( DB_REPLICA );
96  $lang = $context->getLanguage();
97 
98  // Batched version of ResourceLoaderModule::getFileDependencies
99  $vary = ResourceLoaderModule::getVary( $context );
100  $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [
101  'md_module' => $moduleNames,
102  'md_skin' => $vary,
103  ], __METHOD__
104  );
105 
106  // Prime in-object cache for file dependencies
107  $modulesWithDeps = [];
108  foreach ( $res as $row ) {
109  $module = $this->getModule( $row->md_module );
110  if ( $module ) {
111  $module->setFileDependencies( $context, ResourceLoaderModule::expandRelativePaths(
112  json_decode( $row->md_deps, true )
113  ) );
114  $modulesWithDeps[] = $row->md_module;
115  }
116  }
117  // Register the absence of a dependency row too
118  foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) {
119  $module = $this->getModule( $name );
120  if ( $module ) {
121  $this->getModule( $name )->setFileDependencies( $context, [] );
122  }
123  }
124 
125  // Batched version of ResourceLoaderWikiModule::getTitleInfo
126  ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
127 
128  // Prime in-object cache for message blobs for modules with messages
129  $modules = [];
130  foreach ( $moduleNames as $name ) {
131  $module = $this->getModule( $name );
132  if ( $module && $module->getMessages() ) {
133  $modules[$name] = $module;
134  }
135  }
136  $store = $this->getMessageBlobStore();
137  $blobs = $store->getBlobs( $modules, $lang );
138  foreach ( $blobs as $name => $blob ) {
140  }
141  }
142 
160  public static function filter( $filter, $data, array $options = [] ) {
161  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
162  return $data;
163  }
164 
165  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
166  return self::applyFilter( $filter, $data );
167  }
168 
169  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
171 
172  $key = $cache->makeGlobalKey(
173  'resourceloader-filter',
174  $filter,
175  self::CACHE_VERSION,
176  md5( $data )
177  );
178 
179  $result = $cache->get( $key );
180  if ( $result === false ) {
181  $stats->increment( "resourceloader_cache.$filter.miss" );
182  $result = self::applyFilter( $filter, $data );
183  $cache->set( $key, $result, 24 * 3600 );
184  } else {
185  $stats->increment( "resourceloader_cache.$filter.hit" );
186  }
187  if ( $result === null ) {
188  // Cached failure
189  $result = $data;
190  }
191 
192  return $result;
193  }
194 
195  private static function applyFilter( $filter, $data ) {
196  $data = trim( $data );
197  if ( $data ) {
198  try {
199  $data = ( $filter === 'minify-css' )
200  ? CSSMin::minify( $data )
201  : JavaScriptMinifier::minify( $data );
202  } catch ( Exception $e ) {
204  return null;
205  }
206  }
207  return $data;
208  }
209 
215  public function __construct( Config $config = null, LoggerInterface $logger = null ) {
216  $this->logger = $logger ?: new NullLogger();
217 
218  if ( !$config ) {
219  wfDeprecated( __METHOD__ . ' without a Config instance', '1.34' );
220  $config = MediaWikiServices::getInstance()->getMainConfig();
221  }
222  $this->config = $config;
223 
224  // Add 'local' source first
225  $this->addSource( 'local', $config->get( 'LoadScript' ) );
226 
227  // Special module that always exists
228  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
229 
230  $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
231  }
232 
236  public function getConfig() {
237  return $this->config;
238  }
239 
244  public function setLogger( LoggerInterface $logger ) {
245  $this->logger = $logger;
246  }
247 
252  public function getLogger() {
253  return $this->logger;
254  }
255 
260  public function getMessageBlobStore() {
261  return $this->blobStore;
262  }
263 
269  $this->blobStore = $blobStore;
270  }
271 
283  public function register( $name, $info = null ) {
284  $moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
285 
286  // Allow multiple modules to be registered in one call
287  $registrations = is_array( $name ) ? $name : [ $name => $info ];
288  foreach ( $registrations as $name => $info ) {
289  // Warn on duplicate registrations
290  if ( isset( $this->moduleInfos[$name] ) ) {
291  // A module has already been registered by this name
292  $this->logger->warning(
293  'ResourceLoader duplicate registration warning. ' .
294  'Another module has already been registered as ' . $name
295  );
296  }
297 
298  // Check validity
299  if ( !self::isValidModuleName( $name ) ) {
300  throw new MWException( "ResourceLoader module name '$name' is invalid, "
301  . "see ResourceLoader::isValidModuleName()" );
302  }
303  if ( !is_array( $info ) ) {
304  throw new InvalidArgumentException(
305  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
306  );
307  }
308 
309  // Attach module
310  $this->moduleInfos[$name] = $info;
311 
312  // Last-minute changes
313  // Apply custom skin-defined styles to existing modules.
314  if ( $this->isFileModule( $name ) ) {
315  foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
316  // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
317  if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
318  continue;
319  }
320 
321  // If $name is preceded with a '+', the defined style files will be added to 'default'
322  // skinStyles, otherwise 'default' will be ignored as it normally would be.
323  if ( isset( $skinStyles[$name] ) ) {
324  $paths = (array)$skinStyles[$name];
325  $styleFiles = [];
326  } elseif ( isset( $skinStyles['+' . $name] ) ) {
327  $paths = (array)$skinStyles['+' . $name];
328  $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
329  (array)$this->moduleInfos[$name]['skinStyles']['default'] :
330  [];
331  } else {
332  continue;
333  }
334 
335  // Add new file paths, remapping them to refer to our directories and not use settings
336  // from the module we're modifying, which come from the base definition.
337  list( $localBasePath, $remoteBasePath ) =
339 
340  foreach ( $paths as $path ) {
341  $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
342  }
343 
344  $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
345  }
346  }
347  }
348  }
349 
354  public function registerTestModules() {
355  global $IP;
356 
357  if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
358  throw new MWException( 'Attempt to register JavaScript test modules '
359  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
360  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
361  }
362 
363  // This has a 'qunit' key for compat with the below hook.
364  $testModulesMeta = [ 'qunit' => [] ];
365 
366  // Get test suites from extensions
367  // Avoid PHP 7.1 warning from passing $this by reference
368  $rl = $this;
369  Hooks::run( 'ResourceLoaderTestModules', [ &$testModulesMeta, &$rl ] );
370  $extRegistry = ExtensionRegistry::getInstance();
371  // In case of conflict, the deprecated hook has precedence.
372  $testModules = $testModulesMeta['qunit'] + $extRegistry->getAttribute( 'QUnitTestModules' );
373 
375  foreach ( $testModules as $name => &$module ) {
376  // Turn any single-module dependency into an array
377  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
378  $module['dependencies'] = [ $module['dependencies'] ];
379  }
380 
381  // Ensure the testrunner loads before any test suites
382  $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
383 
384  // Keep track of the test suites to load on SpecialJavaScriptTest
386  }
387 
388  // Core test suites (their names have further precedence).
389  $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
390  $testSuiteModuleNames[] = 'test.mediawiki.qunit.suites';
391 
392  $this->register( $testModules );
393  $this->testSuiteModuleNames = $testSuiteModuleNames;
394  }
395 
406  public function addSource( $id, $loadUrl = null ) {
407  // Allow multiple sources to be registered in one call
408  if ( is_array( $id ) ) {
409  foreach ( $id as $key => $value ) {
410  $this->addSource( $key, $value );
411  }
412  return;
413  }
414 
415  // Disallow duplicates
416  if ( isset( $this->sources[$id] ) ) {
417  throw new MWException(
418  'ResourceLoader duplicate source addition error. ' .
419  'Another source has already been registered as ' . $id
420  );
421  }
422 
423  // Pre 1.24 backwards-compatibility
424  if ( is_array( $loadUrl ) ) {
425  if ( !isset( $loadUrl['loadScript'] ) ) {
426  throw new MWException(
427  __METHOD__ . ' was passed an array with no "loadScript" key.'
428  );
429  }
430 
431  $loadUrl = $loadUrl['loadScript'];
432  }
433 
434  $this->sources[$id] = $loadUrl;
435  }
436 
442  public function getModuleNames() {
443  return array_keys( $this->moduleInfos );
444  }
445 
453  public function getTestSuiteModuleNames() {
455  }
456 
464  public function isModuleRegistered( $name ) {
465  return isset( $this->moduleInfos[$name] );
466  }
467 
479  public function getModule( $name ) {
480  if ( !isset( $this->modules[$name] ) ) {
481  if ( !isset( $this->moduleInfos[$name] ) ) {
482  // No such module
483  return null;
484  }
485  // Construct the requested module object
486  $info = $this->moduleInfos[$name];
487  if ( isset( $info['factory'] ) ) {
489  $object = call_user_func( $info['factory'], $info );
490  } else {
491  $class = $info['class'] ?? ResourceLoaderFileModule::class;
493  $object = new $class( $info );
494  }
495  $object->setConfig( $this->getConfig() );
496  $object->setLogger( $this->logger );
497  $object->setName( $name );
498  $this->modules[$name] = $object;
499  }
500 
501  return $this->modules[$name];
502  }
503 
510  protected function isFileModule( $name ) {
511  if ( !isset( $this->moduleInfos[$name] ) ) {
512  return false;
513  }
514  $info = $this->moduleInfos[$name];
515  return !isset( $info['factory'] ) && (
516  // The implied default for 'class' is ResourceLoaderFileModule
517  !isset( $info['class'] ) ||
518  // Explicit default
519  $info['class'] === ResourceLoaderFileModule::class ||
520  is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
521  );
522  }
523 
529  public function getSources() {
530  return $this->sources;
531  }
532 
542  public function getLoadScript( $source ) {
543  if ( !isset( $this->sources[$source] ) ) {
544  throw new MWException( "The $source source was never registered in ResourceLoader." );
545  }
546  return $this->sources[$source];
547  }
548 
554  public static function makeHash( $value ) {
555  $hash = hash( 'fnv132', $value );
556  return Wikimedia\base_convert( $hash, 16, 36, 7 );
557  }
558 
568  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
570  $this->logger->warning(
571  $msg,
572  $context + [ 'exception' => $e ]
573  );
574  $this->errors[] = self::formatExceptionNoComment( $e );
575  }
576 
585  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
586  if ( !$moduleNames ) {
587  return '';
588  }
589  $hashes = array_map( function ( $module ) use ( $context ) {
590  try {
591  return $this->getModule( $module )->getVersionHash( $context );
592  } catch ( Exception $e ) {
593  // If modules fail to compute a version, don't fail the request (T152266)
594  // and still compute versions of other modules.
595  $this->outputErrorAndLog( $e,
596  'Calculating version for "{module}" failed: {exception}',
597  [
598  'module' => $module,
599  ]
600  );
601  return '';
602  }
603  }, $moduleNames );
604  return self::makeHash( implode( '', $hashes ) );
605  }
606 
621  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
622  // version hashes. There is no technical reason for this to be same, and for years the
623  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
624  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
625  // query parameter), then this method must continue to match the JS one.
626  $moduleNames = [];
627  foreach ( $context->getModules() as $name ) {
628  if ( !$this->getModule( $name ) ) {
629  // If a versioned request contains a missing module, the version is a mismatch
630  // as the client considered a module (and version) we don't have.
631  return '';
632  }
633  $moduleNames[] = $name;
634  }
635  return $this->getCombinedVersion( $context, $moduleNames );
636  }
637 
644  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
645  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
646  // is used: ob_clean() will clear the GZIP header in that case and it won't come
647  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
648  // the whole thing in our own output buffer to be sure the active buffer
649  // doesn't use ob_gzhandler.
650  // See https://bugs.php.net/bug.php?id=36514
651  ob_start();
652 
653  $this->measureResponseTime( RequestContext::getMain()->getTiming() );
654 
655  // Find out which modules are missing and instantiate the others
656  $modules = [];
657  $missing = [];
658  foreach ( $context->getModules() as $name ) {
659  $module = $this->getModule( $name );
660  if ( $module ) {
661  // Do not allow private modules to be loaded from the web.
662  // This is a security issue, see T36907.
663  if ( $module->getGroup() === 'private' ) {
664  $this->logger->debug( "Request for private module '$name' denied" );
665  $this->errors[] = "Cannot show private module \"$name\"";
666  continue;
667  }
668  $modules[$name] = $module;
669  } else {
670  $missing[] = $name;
671  }
672  }
673 
674  try {
675  // Preload for getCombinedVersion() and for batch makeModuleResponse()
676  $this->preloadModuleInfo( array_keys( $modules ), $context );
677  } catch ( Exception $e ) {
678  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
679  }
680 
681  // Combine versions to propagate cache invalidation
682  $versionHash = '';
683  try {
684  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
685  } catch ( Exception $e ) {
686  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
687  }
688 
689  // See RFC 2616 § 3.11 Entity Tags
690  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
691  $etag = 'W/"' . $versionHash . '"';
692 
693  // Try the client-side cache first
694  if ( $this->tryRespondNotModified( $context, $etag ) ) {
695  return; // output handled (buffers cleared)
696  }
697 
698  // Use file cache if enabled and available...
699  if ( $this->config->get( 'UseFileCache' ) ) {
700  $fileCache = ResourceFileCache::newFromContext( $context );
701  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
702  return; // output handled
703  }
704  } else {
705  $fileCache = null;
706  }
707 
708  // Generate a response
709  $response = $this->makeModuleResponse( $context, $modules, $missing );
710 
711  // Capture any PHP warnings from the output buffer and append them to the
712  // error list if we're in debug mode.
713  if ( $context->getDebug() ) {
714  $warnings = ob_get_contents();
715  if ( strlen( $warnings ) ) {
716  $this->errors[] = $warnings;
717  }
718  }
719 
720  // Consider saving the response to file cache (unless there are errors).
721  if ( $fileCache &&
722  !$this->errors &&
723  $missing === [] &&
725  ) {
726  if ( $fileCache->isCacheWorthy() ) {
727  // There were enough hits, save the response to the cache
728  $fileCache->saveText( $response );
729  } else {
730  $fileCache->incrMissesRecent( $context->getRequest() );
731  }
732  }
733 
734  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
735 
736  // Remove the output buffer and output the response
737  ob_end_clean();
738 
739  if ( $context->getImageObj() && $this->errors ) {
740  // We can't show both the error messages and the response when it's an image.
741  $response = implode( "\n\n", $this->errors );
742  } elseif ( $this->errors ) {
743  $errorText = implode( "\n\n", $this->errors );
744  $errorResponse = self::makeComment( $errorText );
745  if ( $context->shouldIncludeScripts() ) {
746  $errorResponse .= 'if (window.console && console.error) {'
747  . Xml::encodeJsCall( 'console.error', [ $errorText ] )
748  . "}\n";
749  }
750 
751  // Prepend error info to the response
752  $response = $errorResponse . $response;
753  }
754 
755  $this->errors = [];
756  echo $response;
757  }
758 
759  protected function measureResponseTime( Timing $timing ) {
760  DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
761  $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
762  if ( $measure !== false ) {
763  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
764  $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
765  }
766  } );
767  }
768 
780  protected function sendResponseHeaders(
781  ResourceLoaderContext $context, $etag, $errors, array $extra = []
782  ) {
784  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
785  // Use a short cache expiry so that updates propagate to clients quickly, if:
786  // - No version specified (shared resources, e.g. stylesheets)
787  // - There were errors (recover quickly)
788  // - Version mismatch (T117587, T47877)
789  if ( is_null( $context->getVersion() )
790  || $errors
791  || $context->getVersion() !== $this->makeVersionQuery( $context )
792  ) {
793  $maxage = $rlMaxage['unversioned']['client'];
794  $smaxage = $rlMaxage['unversioned']['server'];
795  // If a version was specified we can use a longer expiry time since changing
796  // version numbers causes cache misses
797  } else {
798  $maxage = $rlMaxage['versioned']['client'];
799  $smaxage = $rlMaxage['versioned']['server'];
800  }
801  if ( $context->getImageObj() ) {
802  // Output different headers if we're outputting textual errors.
803  if ( $errors ) {
804  header( 'Content-Type: text/plain; charset=utf-8' );
805  } else {
806  $context->getImageObj()->sendResponseHeaders( $context );
807  }
808  } elseif ( $context->getOnly() === 'styles' ) {
809  header( 'Content-Type: text/css; charset=utf-8' );
810  header( 'Access-Control-Allow-Origin: *' );
811  } else {
812  header( 'Content-Type: text/javascript; charset=utf-8' );
813  }
814  // See RFC 2616 § 14.19 ETag
815  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
816  header( 'ETag: ' . $etag );
817  if ( $context->getDebug() ) {
818  // Do not cache debug responses
819  header( 'Cache-Control: private, no-cache, must-revalidate' );
820  header( 'Pragma: no-cache' );
821  } else {
822  header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
823  $exp = min( $maxage, $smaxage );
824  header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
825  }
826  foreach ( $extra as $header ) {
827  header( $header );
828  }
829  }
830 
841  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
842  // See RFC 2616 § 14.26 If-None-Match
843  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
844  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
845  // Never send 304s in debug mode
846  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
847  // There's another bug in ob_gzhandler (see also the comment at
848  // the top of this function) that causes it to gzip even empty
849  // responses, meaning it's impossible to produce a truly empty
850  // response (because the gzip header is always there). This is
851  // a problem because 304 responses have to be completely empty
852  // per the HTTP spec, and Firefox behaves buggily when they're not.
853  // See also https://bugs.php.net/bug.php?id=51579
854  // To work around this, we tear down all output buffering before
855  // sending the 304.
856  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
857 
858  HttpStatus::header( 304 );
859 
860  $this->sendResponseHeaders( $context, $etag, false );
861  return true;
862  }
863  return false;
864  }
865 
874  protected function tryRespondFromFileCache(
875  ResourceFileCache $fileCache,
877  $etag
878  ) {
879  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
880  // Buffer output to catch warnings.
881  ob_start();
882  // Get the maximum age the cache can be
883  $maxage = is_null( $context->getVersion() )
884  ? $rlMaxage['unversioned']['server']
885  : $rlMaxage['versioned']['server'];
886  // Minimum timestamp the cache file must have
887  $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
888  if ( !$good ) {
889  try { // RL always hits the DB on file cache miss...
890  wfGetDB( DB_REPLICA );
891  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
892  $good = $fileCache->isCacheGood(); // cache existence check
893  }
894  }
895  if ( $good ) {
896  $ts = $fileCache->cacheTimestamp();
897  // Send content type and cache headers
898  $this->sendResponseHeaders( $context, $etag, false );
899  $response = $fileCache->fetchText();
900  // Capture any PHP warnings from the output buffer and append them to the
901  // response in a comment if we're in debug mode.
902  if ( $context->getDebug() ) {
903  $warnings = ob_get_contents();
904  if ( strlen( $warnings ) ) {
905  $response = self::makeComment( $warnings ) . $response;
906  }
907  }
908  // Remove the output buffer and output the response
909  ob_end_clean();
910  echo $response . "\n/* Cached {$ts} */";
911  return true; // cache hit
912  }
913  // Clear buffer
914  ob_end_clean();
915 
916  return false; // cache miss
917  }
918 
927  public static function makeComment( $text ) {
928  $encText = str_replace( '*/', '* /', $text );
929  return "/*\n$encText\n*/\n";
930  }
931 
938  public static function formatException( $e ) {
939  return self::makeComment( self::formatExceptionNoComment( $e ) );
940  }
941 
949  protected static function formatExceptionNoComment( $e ) {
951 
952  if ( !$wgShowExceptionDetails ) {
954  }
955 
957  "\nBacktrace:\n" .
959  }
960 
973  array $modules, array $missing = []
974  ) {
975  $out = '';
976  $states = [];
977 
978  if ( $modules === [] && $missing === [] ) {
979  return <<<MESSAGE
980 /* This file is the Web entry point for MediaWiki's ResourceLoader:
981  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
982  no modules were requested. Max made me put this here. */
983 MESSAGE;
984  }
985 
986  $image = $context->getImageObj();
987  if ( $image ) {
988  $data = $image->getImageData( $context );
989  if ( $data === false ) {
990  $data = '';
991  $this->errors[] = 'Image generation failed';
992  }
993  return $data;
994  }
995 
996  foreach ( $missing as $name ) {
997  $states[$name] = 'missing';
998  }
999 
1000  $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1001 
1002  foreach ( $modules as $name => $module ) {
1003  try {
1004  $content = $module->getModuleContent( $context );
1005  $implementKey = $name . '@' . $module->getVersionHash( $context );
1006  $strContent = '';
1007 
1008  if ( isset( $content['headers'] ) ) {
1009  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1010  }
1011 
1012  // Append output
1013  switch ( $context->getOnly() ) {
1014  case 'scripts':
1015  $scripts = $content['scripts'];
1016  if ( is_string( $scripts ) ) {
1017  // Load scripts raw...
1018  $strContent = $scripts;
1019  } elseif ( is_array( $scripts ) ) {
1020  // ...except when $scripts is an array of URLs or an associative array
1021  $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
1022  }
1023  break;
1024  case 'styles':
1025  $styles = $content['styles'];
1026  // We no longer separate into media, they are all combined now with
1027  // custom media type groups into @media .. {} sections as part of the css string.
1028  // Module returns either an empty array or a numerical array with css strings.
1029  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1030  break;
1031  default:
1032  $scripts = $content['scripts'] ?? '';
1033  if ( is_string( $scripts ) ) {
1034  if ( $name === 'site' || $name === 'user' ) {
1035  // Legacy scripts that run in the global scope without a closure.
1036  // mw.loader.implement will use globalEval if scripts is a string.
1037  // Minify manually here, because general response minification is
1038  // not effective due it being a string literal, not a function.
1039  if ( !$context->getDebug() ) {
1040  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1041  }
1042  } else {
1043  $scripts = new XmlJsCode( $scripts );
1044  }
1045  }
1046  $strContent = self::makeLoaderImplementScript(
1047  $implementKey,
1048  $scripts,
1049  $content['styles'] ?? [],
1050  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1051  $content['templates'] ?? []
1052  );
1053  break;
1054  }
1055 
1056  if ( !$context->getDebug() ) {
1057  $strContent = self::filter( $filter, $strContent );
1058  } else {
1059  // In debug mode, separate each response by a new line.
1060  // For example, between 'mw.loader.implement();' statements.
1061  $strContent = $this->ensureNewline( $strContent );
1062  }
1063 
1064  if ( $context->getOnly() === 'scripts' ) {
1065  // Use a linebreak between module scripts (T162719)
1066  $out .= $this->ensureNewline( $strContent );
1067  } else {
1068  $out .= $strContent;
1069  }
1070 
1071  } catch ( Exception $e ) {
1072  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1073 
1074  // Respond to client with error-state instead of module implementation
1075  $states[$name] = 'error';
1076  unset( $modules[$name] );
1077  }
1078  }
1079 
1080  // Update module states
1081  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1082  if ( $modules && $context->getOnly() === 'scripts' ) {
1083  // Set the state of modules loaded as only scripts to ready as
1084  // they don't have an mw.loader.implement wrapper that sets the state
1085  foreach ( $modules as $name => $module ) {
1086  $states[$name] = 'ready';
1087  }
1088  }
1089 
1090  // Set the state of modules we didn't respond to with mw.loader.implement
1091  if ( $states ) {
1092  $stateScript = self::makeLoaderStateScript( $states );
1093  if ( !$context->getDebug() ) {
1094  $stateScript = self::filter( 'minify-js', $stateScript );
1095  }
1096  // Use a linebreak between module script and state script (T162719)
1097  $out = $this->ensureNewline( $out ) . $stateScript;
1098  }
1099  } elseif ( $states ) {
1100  $this->errors[] = 'Problematic modules: '
1101  . self::encodeJsonForScript( $states );
1102  }
1103 
1104  return $out;
1105  }
1106 
1112  private function ensureNewline( $str ) {
1113  $end = substr( $str, -1 );
1114  if ( $end === false || $end === '' || $end === "\n" ) {
1115  return $str;
1116  }
1117  return $str . "\n";
1118  }
1119 
1126  public function getModulesByMessage( $messageKey ) {
1127  $moduleNames = [];
1128  foreach ( $this->getModuleNames() as $moduleName ) {
1129  $module = $this->getModule( $moduleName );
1130  if ( in_array( $messageKey, $module->getMessages() ) ) {
1131  $moduleNames[] = $moduleName;
1132  }
1133  }
1134  return $moduleNames;
1135  }
1136 
1154  protected static function makeLoaderImplementScript(
1155  $name, $scripts, $styles, $messages, $templates
1156  ) {
1157  if ( $scripts instanceof XmlJsCode ) {
1158  if ( $scripts->value === '' ) {
1159  $scripts = null;
1160  } elseif ( self::inDebugMode() ) {
1161  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1162  } else {
1163  $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
1164  }
1165  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1166  $files = $scripts['files'];
1167  foreach ( $files as $path => &$file ) {
1168  // $file is changed (by reference) from a descriptor array to the content of the file
1169  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1170  if ( $file['type'] === 'script' ) {
1171  // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1172  if ( self::inDebugMode() ) {
1173  $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
1174  } else {
1175  $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
1176  }
1177  } else {
1178  $file = $file['content'];
1179  }
1180  }
1181  $scripts = XmlJsCode::encodeObject( [
1182  'main' => $scripts['main'],
1183  'files' => XmlJsCode::encodeObject( $files, self::inDebugMode() )
1184  ], self::inDebugMode() );
1185  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1186  throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
1187  }
1188 
1189  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1190  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1191  // of "{}". Force them to objects.
1192  $module = [
1193  $name,
1194  $scripts,
1195  (object)$styles,
1196  (object)$messages,
1197  (object)$templates
1198  ];
1199  self::trimArray( $module );
1200 
1201  return Xml::encodeJsCall( 'mw.loader.implement', $module, self::inDebugMode() );
1202  }
1203 
1211  public static function makeMessageSetScript( $messages ) {
1212  return 'mw.messages.set('
1213  . self::encodeJsonForScript( (object)$messages )
1214  . ');';
1215  }
1216 
1224  public static function makeCombinedStyles( array $stylePairs ) {
1225  $out = [];
1226  foreach ( $stylePairs as $media => $styles ) {
1227  // ResourceLoaderFileModule::getStyle can return the styles
1228  // as a string or an array of strings. This is to allow separation in
1229  // the front-end.
1230  $styles = (array)$styles;
1231  foreach ( $styles as $style ) {
1232  $style = trim( $style );
1233  // Don't output an empty "@media print { }" block (T42498)
1234  if ( $style !== '' ) {
1235  // Transform the media type based on request params and config
1236  // The way that this relies on $wgRequest to propagate request params is slightly evil
1237  $media = OutputPage::transformCssMedia( $media );
1238 
1239  if ( $media === '' || $media == 'all' ) {
1240  $out[] = $style;
1241  } elseif ( is_string( $media ) ) {
1242  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1243  }
1244  // else: skip
1245  }
1246  }
1247  }
1248  return $out;
1249  }
1250 
1260  public static function encodeJsonForScript( $data ) {
1261  // Keep output as small as possible by disabling needless escape modes
1262  // that PHP uses by default.
1263  // However, while most module scripts are only served on HTTP responses
1264  // for JavaScript, some modules can also be embedded in the HTML as inline
1265  // scripts. This, and the fact that we sometimes need to export strings
1266  // containing user-generated content and labels that may genuinely contain
1267  // a sequences like "</script>", we need to encode either '/' or '<'.
1268  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1269  // and allows URLs to mostly remain readable.
1270  $jsonFlags = JSON_UNESCAPED_SLASHES |
1271  JSON_UNESCAPED_UNICODE |
1272  JSON_HEX_TAG |
1273  JSON_HEX_AMP;
1274  if ( self::inDebugMode() ) {
1275  $jsonFlags |= JSON_PRETTY_PRINT;
1276  }
1277  return json_encode( $data, $jsonFlags );
1278  }
1279 
1294  public static function makeLoaderStateScript( $states, $state = null ) {
1295  if ( !is_array( $states ) ) {
1296  $states = [ $states => $state ];
1297  }
1298  return 'mw.loader.state('
1299  . self::encodeJsonForScript( $states )
1300  . ');';
1301  }
1302 
1303  private static function isEmptyObject( stdClass $obj ) {
1304  foreach ( $obj as $key => $value ) {
1305  return false;
1306  }
1307  return true;
1308  }
1309 
1322  private static function trimArray( array &$array ) {
1323  $i = count( $array );
1324  while ( $i-- ) {
1325  if ( $array[$i] === null
1326  || $array[$i] === []
1327  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1328  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1329  ) {
1330  unset( $array[$i] );
1331  } else {
1332  break;
1333  }
1334  }
1335  }
1336 
1362  public static function makeLoaderRegisterScript( array $modules ) {
1363  // Optimisation: Transform dependency names into indexes when possible
1364  // to produce smaller output. They are expanded by mw.loader.register on
1365  // the other end using resolveIndexedDependencies().
1366  $index = [];
1367  foreach ( $modules as $i => &$module ) {
1368  // Build module name index
1369  $index[$module[0]] = $i;
1370  }
1371  foreach ( $modules as &$module ) {
1372  if ( isset( $module[2] ) ) {
1373  foreach ( $module[2] as &$dependency ) {
1374  if ( isset( $index[$dependency] ) ) {
1375  // Replace module name in dependency list with index
1376  $dependency = $index[$dependency];
1377  }
1378  }
1379  }
1380  }
1381 
1382  array_walk( $modules, [ self::class, 'trimArray' ] );
1383 
1384  return 'mw.loader.register('
1385  . self::encodeJsonForScript( $modules )
1386  . ');';
1387  }
1388 
1403  public static function makeLoaderSourcesScript( $sources, $loadUrl = null ) {
1404  if ( !is_array( $sources ) ) {
1405  $sources = [ $sources => $loadUrl ];
1406  }
1407  return 'mw.loader.addSource('
1408  . self::encodeJsonForScript( $sources )
1409  . ');';
1410  }
1411 
1418  public static function makeLoaderConditionalScript( $script ) {
1419  // Adds a function to lazy-created RLQ
1420  return '(RLQ=window.RLQ||[]).push(function(){' .
1421  trim( $script ) . '});';
1422  }
1423 
1432  public static function makeInlineCodeWithModule( $modules, $script ) {
1433  // Adds an array to lazy-created RLQ
1434  return '(RLQ=window.RLQ||[]).push(['
1435  . self::encodeJsonForScript( $modules ) . ','
1436  . 'function(){' . trim( $script ) . '}'
1437  . ']);';
1438  }
1439 
1451  public static function makeInlineScript( $script, $nonce = null ) {
1452  $js = self::makeLoaderConditionalScript( $script );
1453  $escNonce = '';
1454  if ( $nonce === null ) {
1455  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1456  } elseif ( $nonce !== false ) {
1457  // If it was false, CSP is disabled, so no nonce attribute.
1458  // Nonce should be only base64 characters, so should be safe,
1459  // but better to be safely escaped than sorry.
1460  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1461  }
1462 
1463  return new WrappedString(
1464  Html::inlineScript( $js, $nonce ),
1465  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1466  '});</script>'
1467  );
1468  }
1469 
1478  public static function makeConfigSetScript( array $configuration ) {
1479  $js = Xml::encodeJsCall(
1480  'mw.config.set',
1481  [ $configuration ],
1482  self::inDebugMode()
1483  );
1484  if ( $js === false ) {
1485  $e = new Exception(
1486  'JSON serialization of config data failed. ' .
1487  'This usually means the config data is not valid UTF-8.'
1488  );
1490  $js = Xml::encodeJsCall( 'mw.log.error', [ $e->__toString() ] );
1491  }
1492  return $js;
1493  }
1494 
1508  public static function makePackedModulesString( $modules ) {
1509  $moduleMap = []; // [ prefix => [ suffixes ] ]
1510  foreach ( $modules as $module ) {
1511  $pos = strrpos( $module, '.' );
1512  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1513  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1514  $moduleMap[$prefix][] = $suffix;
1515  }
1516 
1517  $arr = [];
1518  foreach ( $moduleMap as $prefix => $suffixes ) {
1519  $p = $prefix === '' ? '' : $prefix . '.';
1520  $arr[] = $p . implode( ',', $suffixes );
1521  }
1522  return implode( '|', $arr );
1523  }
1524 
1536  public static function expandModuleNames( $modules ) {
1537  $retval = [];
1538  $exploded = explode( '|', $modules );
1539  foreach ( $exploded as $group ) {
1540  if ( strpos( $group, ',' ) === false ) {
1541  // This is not a set of modules in foo.bar,baz notation
1542  // but a single module
1543  $retval[] = $group;
1544  } else {
1545  // This is a set of modules in foo.bar,baz notation
1546  $pos = strrpos( $group, '.' );
1547  if ( $pos === false ) {
1548  // Prefixless modules, i.e. without dots
1549  $retval = array_merge( $retval, explode( ',', $group ) );
1550  } else {
1551  // We have a prefix and a bunch of suffixes
1552  $prefix = substr( $group, 0, $pos ); // 'foo'
1553  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1554  foreach ( $suffixes as $suffix ) {
1555  $retval[] = "$prefix.$suffix";
1556  }
1557  }
1558  }
1559  }
1560  return $retval;
1561  }
1562 
1568  public static function inDebugMode() {
1569  if ( self::$debugMode === null ) {
1571  self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
1572  $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
1573  );
1574  }
1575  return self::$debugMode;
1576  }
1577 
1588  public static function clearCache() {
1589  self::$debugMode = null;
1590  }
1591 
1602  $extraQuery = []
1603  ) {
1604  $query = self::createLoaderQuery( $context, $extraQuery );
1605  $script = $this->getLoadScript( $source );
1606 
1607  return wfAppendQuery( $script, $query );
1608  }
1609 
1619  protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
1620  return self::makeLoaderQuery(
1621  $context->getModules(),
1622  $context->getLanguage(),
1623  $context->getSkin(),
1624  $context->getUser(),
1625  $context->getVersion(),
1626  $context->getDebug(),
1627  $context->getOnly(),
1628  $context->getRequest()->getBool( 'printable' ),
1629  $context->getRequest()->getBool( 'handheld' ),
1630  $extraQuery
1631  );
1632  }
1633 
1650  public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
1651  $version = null, $debug = false, $only = null, $printable = false,
1652  $handheld = false, $extraQuery = []
1653  ) {
1654  $query = [
1655  'modules' => self::makePackedModulesString( $modules ),
1656  ];
1657  // Keep urls short by omitting query parameters that
1658  // match the defaults assumed by ResourceLoaderContext.
1659  // Note: This relies on the defaults either being insignificant or forever constant,
1660  // as otherwise cached urls could change in meaning when the defaults change.
1662  $query['lang'] = $lang;
1663  }
1665  $query['skin'] = $skin;
1666  }
1667  if ( $debug === true ) {
1668  $query['debug'] = 'true';
1669  }
1670  if ( $user !== null ) {
1671  $query['user'] = $user;
1672  }
1673  if ( $version !== null ) {
1674  $query['version'] = $version;
1675  }
1676  if ( $only !== null ) {
1677  $query['only'] = $only;
1678  }
1679  if ( $printable ) {
1680  $query['printable'] = 1;
1681  }
1682  if ( $handheld ) {
1683  $query['handheld'] = 1;
1684  }
1685  $query += $extraQuery;
1686 
1687  // Make queries uniform in order
1688  ksort( $query );
1689  return $query;
1690  }
1691 
1701  public static function isValidModuleName( $moduleName ) {
1702  return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName );
1703  }
1704 
1715  public function getLessCompiler( $vars = [] ) {
1716  global $IP;
1717  // When called from the installer, it is possible that a required PHP extension
1718  // is missing (at least for now; see T49564). If this is the case, throw an
1719  // exception (caught by the installer) to prevent a fatal error later on.
1720  if ( !class_exists( 'Less_Parser' ) ) {
1721  throw new MWException( 'MediaWiki requires the less.php parser' );
1722  }
1723 
1724  $parser = new Less_Parser;
1725  $parser->ModifyVars( $vars );
1726  $parser->SetImportDirs( [
1727  "$IP/resources/src/mediawiki.less/" => '',
1728  ] );
1729  $parser->SetOption( 'relativeUrls', false );
1730 
1731  return $parser;
1732  }
1733 
1741  public function getLessVars() {
1742  return [];
1743  }
1744 }
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
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used $groups Array of ChangesListFilterGroup objects(added in 1.34) 'FileDeleteComplete' 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:1529
static filter( $filter, $data, array $options=[])
Run JavaScript or CSS data through a filter, caching the filtered result for future calls...
ResourceLoaderModule [] $modules
Map of (module name => ResourceLoaderModule)
getSources()
Get the list of sources.
$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:2147
createLoaderURL( $source, ResourceLoaderContext $context, $extraQuery=[])
Build a load.php URL.
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:767
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:572
getTestSuiteModuleNames()
Get a list of module names with QUnit test suites.
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:1781
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.
setMessageBlob( $blob, $lang)
Set in-object cache for message blobs.
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. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\*-\*)") will be honored when streaming the file. '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:1970
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:767
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:1972
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.
$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:1972
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:767
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:1728
$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:1972
$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.
$debug
Definition: Setup.php:808
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:767
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
Map of (source => path); E.g.
static makePackedModulesString( $modules)
Convert an array of module names to a packed query string.
string [] $testSuiteModuleNames
List of module names that contain QUnit test suites.
static makeLoaderSourcesScript( $sources, $loadUrl=null)
Returns JS code which calls mw.loader.addSource() with the given parameters.
const CACHE_ANYTHING
Definition: Defines.php:81
setLogger(LoggerInterface $logger)
LoggerInterface $logger
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2621
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...
string [] $extraHeaders
Extra HTTP response headers from modules loaded in makeModuleResponse()
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.
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:776
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.
static getVary(ResourceLoaderContext $context)
Get vary string.
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
$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:2205
static applyFilter( $filter, $data)
static minify( $s)
Returns minified JavaScript code.
array [] $moduleInfos
Map of (module name => associative info array)
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
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:767
Object passed around to modules which contains information about the state of a specific loader reque...