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',
200  'filter',
201  $filter,
202  self::CACHE_VERSION,
203  md5( $data )
204  );
205 
206  $result = $cache->get( $key );
207  if ( $result === false ) {
208  $stats->increment( "resourceloader_cache.$filter.miss" );
209  $result = self::applyFilter( $filter, $data );
210  $cache->set( $key, $result, 24 * 3600 );
211  } else {
212  $stats->increment( "resourceloader_cache.$filter.hit" );
213  }
214  if ( $result === null ) {
215  // Cached failure
216  $result = $data;
217  }
218 
219  return $result;
220  }
221 
222  private static function applyFilter( $filter, $data ) {
223  $data = trim( $data );
224  if ( $data ) {
225  try {
226  $data = ( $filter === 'minify-css' )
227  ? CSSMin::minify( $data )
229  } catch ( Exception $e ) {
231  return null;
232  }
233  }
234  return $data;
235  }
236 
242  public function __construct( Config $config = null, LoggerInterface $logger = null ) {
243  $this->logger = $logger ?: new NullLogger();
244 
245  if ( !$config ) {
246  // TODO: Deprecate and remove.
247  $this->logger->debug( __METHOD__ . ' was called without providing a Config instance' );
248  $config = MediaWikiServices::getInstance()->getMainConfig();
249  }
250  $this->config = $config;
251 
252  // Add 'local' source first
253  $this->addSource( 'local', $config->get( 'LoadScript' ) );
254 
255  // Special module that always exists
256  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
257 
258  $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
259  }
260 
264  public function getConfig() {
265  return $this->config;
266  }
267 
272  public function setLogger( LoggerInterface $logger ) {
273  $this->logger = $logger;
274  }
275 
280  public function getLogger() {
281  return $this->logger;
282  }
283 
288  public function getMessageBlobStore() {
289  return $this->blobStore;
290  }
291 
297  $this->blobStore = $blobStore;
298  }
299 
311  public function register( $name, $info = null ) {
312  $moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
313 
314  // Allow multiple modules to be registered in one call
315  $registrations = is_array( $name ) ? $name : [ $name => $info ];
316  foreach ( $registrations as $name => $info ) {
317  // Warn on duplicate registrations
318  if ( isset( $this->moduleInfos[$name] ) ) {
319  // A module has already been registered by this name
320  $this->logger->warning(
321  'ResourceLoader duplicate registration warning. ' .
322  'Another module has already been registered as ' . $name
323  );
324  }
325 
326  // Check $name for validity
327  if ( !self::isValidModuleName( $name ) ) {
328  throw new MWException( "ResourceLoader module name '$name' is invalid, "
329  . "see ResourceLoader::isValidModuleName()" );
330  }
331 
332  // Attach module
333  if ( $info instanceof ResourceLoaderModule ) {
334  $this->moduleInfos[$name] = [ 'object' => $info ];
335  $info->setName( $name );
336  $this->modules[$name] = $info;
337  } elseif ( is_array( $info ) ) {
338  // New calling convention
339  $this->moduleInfos[$name] = $info;
340  } else {
341  throw new MWException(
342  'ResourceLoader module info type error for module \'' . $name .
343  '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
344  );
345  }
346 
347  // Last-minute changes
348 
349  // Apply custom skin-defined styles to existing modules.
350  if ( $this->isFileModule( $name ) ) {
351  foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
352  // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
353  if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
354  continue;
355  }
356 
357  // If $name is preceded with a '+', the defined style files will be added to 'default'
358  // skinStyles, otherwise 'default' will be ignored as it normally would be.
359  if ( isset( $skinStyles[$name] ) ) {
360  $paths = (array)$skinStyles[$name];
361  $styleFiles = [];
362  } elseif ( isset( $skinStyles['+' . $name] ) ) {
363  $paths = (array)$skinStyles['+' . $name];
364  $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
365  (array)$this->moduleInfos[$name]['skinStyles']['default'] :
366  [];
367  } else {
368  continue;
369  }
370 
371  // Add new file paths, remapping them to refer to our directories and not use settings
372  // from the module we're modifying, which come from the base definition.
373  list( $localBasePath, $remoteBasePath ) =
375 
376  foreach ( $paths as $path ) {
377  $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
378  }
379 
380  $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
381  }
382  }
383  }
384  }
385 
389  public function registerTestModules() {
390  global $IP;
391 
392  if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
393  throw new MWException( 'Attempt to register JavaScript test modules '
394  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
395  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
396  }
397 
398  $testModules = [
399  'qunit' => [],
400  ];
401 
402  // Get test suites from extensions
403  // Avoid PHP 7.1 warning from passing $this by reference
404  $rl = $this;
405  Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
406  $extRegistry = ExtensionRegistry::getInstance();
407  // In case of conflict, the deprecated hook has precedence.
408  $testModules['qunit'] += $extRegistry->getAttribute( 'QUnitTestModules' );
409 
410  // Add the QUnit testrunner as implicit dependency to extension test suites.
411  foreach ( $testModules['qunit'] as &$module ) {
412  // Shuck any single-module dependency as an array
413  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
414  $module['dependencies'] = [ $module['dependencies'] ];
415  }
416 
417  $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
418  }
419 
420  // Get core test suites
421  $testModules['qunit'] =
422  ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
423 
424  foreach ( $testModules as $id => $names ) {
425  // Register test modules
426  $this->register( $testModules[$id] );
427 
428  // Keep track of their names so that they can be loaded together
429  $this->testModuleNames[$id] = array_keys( $testModules[$id] );
430  }
431  }
432 
443  public function addSource( $id, $loadUrl = null ) {
444  // Allow multiple sources to be registered in one call
445  if ( is_array( $id ) ) {
446  foreach ( $id as $key => $value ) {
447  $this->addSource( $key, $value );
448  }
449  return;
450  }
451 
452  // Disallow duplicates
453  if ( isset( $this->sources[$id] ) ) {
454  throw new MWException(
455  'ResourceLoader duplicate source addition error. ' .
456  'Another source has already been registered as ' . $id
457  );
458  }
459 
460  // Pre 1.24 backwards-compatibility
461  if ( is_array( $loadUrl ) ) {
462  if ( !isset( $loadUrl['loadScript'] ) ) {
463  throw new MWException(
464  __METHOD__ . ' was passed an array with no "loadScript" key.'
465  );
466  }
467 
468  $loadUrl = $loadUrl['loadScript'];
469  }
470 
471  $this->sources[$id] = $loadUrl;
472  }
473 
479  public function getModuleNames() {
480  return array_keys( $this->moduleInfos );
481  }
482 
493  public function getTestModuleNames( $framework = 'all' ) {
495  if ( $framework == 'all' ) {
496  return $this->testModuleNames;
497  } elseif ( isset( $this->testModuleNames[$framework] )
498  && is_array( $this->testModuleNames[$framework] )
499  ) {
500  return $this->testModuleNames[$framework];
501  } else {
502  return [];
503  }
504  }
505 
513  public function isModuleRegistered( $name ) {
514  return isset( $this->moduleInfos[$name] );
515  }
516 
528  public function getModule( $name ) {
529  if ( !isset( $this->modules[$name] ) ) {
530  if ( !isset( $this->moduleInfos[$name] ) ) {
531  // No such module
532  return null;
533  }
534  // Construct the requested object
535  $info = $this->moduleInfos[$name];
537  if ( isset( $info['object'] ) ) {
538  // Object given in info array
539  $object = $info['object'];
540  } elseif ( isset( $info['factory'] ) ) {
541  $object = call_user_func( $info['factory'], $info );
542  $object->setConfig( $this->getConfig() );
543  $object->setLogger( $this->logger );
544  } else {
545  $class = $info['class'] ?? ResourceLoaderFileModule::class;
547  $object = new $class( $info );
548  $object->setConfig( $this->getConfig() );
549  $object->setLogger( $this->logger );
550  }
551  $object->setName( $name );
552  $this->modules[$name] = $object;
553  }
554 
555  return $this->modules[$name];
556  }
557 
564  protected function isFileModule( $name ) {
565  if ( !isset( $this->moduleInfos[$name] ) ) {
566  return false;
567  }
568  $info = $this->moduleInfos[$name];
569  if ( isset( $info['object'] ) ) {
570  return false;
571  }
572  return (
573  // The implied default for 'class' is ResourceLoaderFileModule
574  !isset( $info['class'] ) ||
575  // Explicit default
576  $info['class'] === ResourceLoaderFileModule::class ||
577  is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
578  );
579  }
580 
586  public function getSources() {
587  return $this->sources;
588  }
589 
599  public function getLoadScript( $source ) {
600  if ( !isset( $this->sources[$source] ) ) {
601  throw new MWException( "The $source source was never registered in ResourceLoader." );
602  }
603  return $this->sources[$source];
604  }
605 
611  public static function makeHash( $value ) {
612  $hash = hash( 'fnv132', $value );
613  return Wikimedia\base_convert( $hash, 16, 36, 7 );
614  }
615 
625  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
627  $this->logger->warning(
628  $msg,
629  $context + [ 'exception' => $e ]
630  );
631  $this->errors[] = self::formatExceptionNoComment( $e );
632  }
633 
642  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
643  if ( !$moduleNames ) {
644  return '';
645  }
646  $hashes = array_map( function ( $module ) use ( $context ) {
647  try {
648  return $this->getModule( $module )->getVersionHash( $context );
649  } catch ( Exception $e ) {
650  // If modules fail to compute a version, don't fail the request (T152266)
651  // and still compute versions of other modules.
652  $this->outputErrorAndLog( $e,
653  'Calculating version for "{module}" failed: {exception}',
654  [
655  'module' => $module,
656  ]
657  );
658  return '';
659  }
660  }, $moduleNames );
661  return self::makeHash( implode( '', $hashes ) );
662  }
663 
677  public function makeVersionQuery( ResourceLoaderContext $context ) {
678  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
679  // version hashes. There is no technical reason for this to be same, and for years the
680  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
681  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
682  // query parameter), then this method must continue to match the JS one.
683  $moduleNames = [];
684  foreach ( $context->getModules() as $name ) {
685  if ( !$this->getModule( $name ) ) {
686  // If a versioned request contains a missing module, the version is a mismatch
687  // as the client considered a module (and version) we don't have.
688  return '';
689  }
690  $moduleNames[] = $name;
691  }
692  return $this->getCombinedVersion( $context, $moduleNames );
693  }
694 
700  public function respond( ResourceLoaderContext $context ) {
701  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
702  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
703  // is used: ob_clean() will clear the GZIP header in that case and it won't come
704  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
705  // the whole thing in our own output buffer to be sure the active buffer
706  // doesn't use ob_gzhandler.
707  // See https://bugs.php.net/bug.php?id=36514
708  ob_start();
709 
710  $this->measureResponseTime( RequestContext::getMain()->getTiming() );
711 
712  // Find out which modules are missing and instantiate the others
713  $modules = [];
714  $missing = [];
715  foreach ( $context->getModules() as $name ) {
716  $module = $this->getModule( $name );
717  if ( $module ) {
718  // Do not allow private modules to be loaded from the web.
719  // This is a security issue, see T36907.
720  if ( $module->getGroup() === 'private' ) {
721  $this->logger->debug( "Request for private module '$name' denied" );
722  $this->errors[] = "Cannot show private module \"$name\"";
723  continue;
724  }
725  $modules[$name] = $module;
726  } else {
727  $missing[] = $name;
728  }
729  }
730 
731  try {
732  // Preload for getCombinedVersion() and for batch makeModuleResponse()
733  $this->preloadModuleInfo( array_keys( $modules ), $context );
734  } catch ( Exception $e ) {
735  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
736  }
737 
738  // Combine versions to propagate cache invalidation
739  $versionHash = '';
740  try {
741  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
742  } catch ( Exception $e ) {
743  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
744  }
745 
746  // See RFC 2616 § 3.11 Entity Tags
747  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
748  $etag = 'W/"' . $versionHash . '"';
749 
750  // Try the client-side cache first
751  if ( $this->tryRespondNotModified( $context, $etag ) ) {
752  return; // output handled (buffers cleared)
753  }
754 
755  // Use file cache if enabled and available...
756  if ( $this->config->get( 'UseFileCache' ) ) {
757  $fileCache = ResourceFileCache::newFromContext( $context );
758  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
759  return; // output handled
760  }
761  }
762 
763  // Generate a response
764  $response = $this->makeModuleResponse( $context, $modules, $missing );
765 
766  // Capture any PHP warnings from the output buffer and append them to the
767  // error list if we're in debug mode.
768  if ( $context->getDebug() ) {
769  $warnings = ob_get_contents();
770  if ( strlen( $warnings ) ) {
771  $this->errors[] = $warnings;
772  }
773  }
774 
775  // Save response to file cache unless there are errors
776  if ( isset( $fileCache ) && !$this->errors && $missing === [] ) {
777  // Cache single modules and images...and other requests if there are enough hits
778  if ( ResourceFileCache::useFileCache( $context ) ) {
779  if ( $fileCache->isCacheWorthy() ) {
780  $fileCache->saveText( $response );
781  } else {
782  $fileCache->incrMissesRecent( $context->getRequest() );
783  }
784  }
785  }
786 
787  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
788 
789  // Remove the output buffer and output the response
790  ob_end_clean();
791 
792  if ( $context->getImageObj() && $this->errors ) {
793  // We can't show both the error messages and the response when it's an image.
794  $response = implode( "\n\n", $this->errors );
795  } elseif ( $this->errors ) {
796  $errorText = implode( "\n\n", $this->errors );
797  $errorResponse = self::makeComment( $errorText );
798  if ( $context->shouldIncludeScripts() ) {
799  $errorResponse .= 'if (window.console && console.error) {'
800  . Xml::encodeJsCall( 'console.error', [ $errorText ] )
801  . "}\n";
802  }
803 
804  // Prepend error info to the response
805  $response = $errorResponse . $response;
806  }
807 
808  $this->errors = [];
809  echo $response;
810  }
811 
812  protected function measureResponseTime( Timing $timing ) {
813  DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
814  $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
815  if ( $measure !== false ) {
816  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
817  $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
818  }
819  } );
820  }
821 
833  protected function sendResponseHeaders(
834  ResourceLoaderContext $context, $etag, $errors, array $extra = []
835  ) {
837  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
838  // Use a short cache expiry so that updates propagate to clients quickly, if:
839  // - No version specified (shared resources, e.g. stylesheets)
840  // - There were errors (recover quickly)
841  // - Version mismatch (T117587, T47877)
842  if ( is_null( $context->getVersion() )
843  || $errors
844  || $context->getVersion() !== $this->makeVersionQuery( $context )
845  ) {
846  $maxage = $rlMaxage['unversioned']['client'];
847  $smaxage = $rlMaxage['unversioned']['server'];
848  // If a version was specified we can use a longer expiry time since changing
849  // version numbers causes cache misses
850  } else {
851  $maxage = $rlMaxage['versioned']['client'];
852  $smaxage = $rlMaxage['versioned']['server'];
853  }
854  if ( $context->getImageObj() ) {
855  // Output different headers if we're outputting textual errors.
856  if ( $errors ) {
857  header( 'Content-Type: text/plain; charset=utf-8' );
858  } else {
859  $context->getImageObj()->sendResponseHeaders( $context );
860  }
861  } elseif ( $context->getOnly() === 'styles' ) {
862  header( 'Content-Type: text/css; charset=utf-8' );
863  header( 'Access-Control-Allow-Origin: *' );
864  } else {
865  header( 'Content-Type: text/javascript; charset=utf-8' );
866  }
867  // See RFC 2616 § 14.19 ETag
868  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
869  header( 'ETag: ' . $etag );
870  if ( $context->getDebug() ) {
871  // Do not cache debug responses
872  header( 'Cache-Control: private, no-cache, must-revalidate' );
873  header( 'Pragma: no-cache' );
874  } else {
875  header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
876  $exp = min( $maxage, $smaxage );
877  header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
878  }
879  foreach ( $extra as $header ) {
880  header( $header );
881  }
882  }
883 
894  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
895  // See RFC 2616 § 14.26 If-None-Match
896  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
897  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
898  // Never send 304s in debug mode
899  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
900  // There's another bug in ob_gzhandler (see also the comment at
901  // the top of this function) that causes it to gzip even empty
902  // responses, meaning it's impossible to produce a truly empty
903  // response (because the gzip header is always there). This is
904  // a problem because 304 responses have to be completely empty
905  // per the HTTP spec, and Firefox behaves buggily when they're not.
906  // See also https://bugs.php.net/bug.php?id=51579
907  // To work around this, we tear down all output buffering before
908  // sending the 304.
909  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
910 
911  HttpStatus::header( 304 );
912 
913  $this->sendResponseHeaders( $context, $etag, false );
914  return true;
915  }
916  return false;
917  }
918 
927  protected function tryRespondFromFileCache(
928  ResourceFileCache $fileCache,
929  ResourceLoaderContext $context,
930  $etag
931  ) {
932  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
933  // Buffer output to catch warnings.
934  ob_start();
935  // Get the maximum age the cache can be
936  $maxage = is_null( $context->getVersion() )
937  ? $rlMaxage['unversioned']['server']
938  : $rlMaxage['versioned']['server'];
939  // Minimum timestamp the cache file must have
940  $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
941  if ( !$good ) {
942  try { // RL always hits the DB on file cache miss...
943  wfGetDB( DB_REPLICA );
944  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
945  $good = $fileCache->isCacheGood(); // cache existence check
946  }
947  }
948  if ( $good ) {
949  $ts = $fileCache->cacheTimestamp();
950  // Send content type and cache headers
951  $this->sendResponseHeaders( $context, $etag, false );
952  $response = $fileCache->fetchText();
953  // Capture any PHP warnings from the output buffer and append them to the
954  // response in a comment if we're in debug mode.
955  if ( $context->getDebug() ) {
956  $warnings = ob_get_contents();
957  if ( strlen( $warnings ) ) {
958  $response = self::makeComment( $warnings ) . $response;
959  }
960  }
961  // Remove the output buffer and output the response
962  ob_end_clean();
963  echo $response . "\n/* Cached {$ts} */";
964  return true; // cache hit
965  }
966  // Clear buffer
967  ob_end_clean();
968 
969  return false; // cache miss
970  }
971 
980  public static function makeComment( $text ) {
981  $encText = str_replace( '*/', '* /', $text );
982  return "/*\n$encText\n*/\n";
983  }
984 
991  public static function formatException( $e ) {
992  return self::makeComment( self::formatExceptionNoComment( $e ) );
993  }
994 
1002  protected static function formatExceptionNoComment( $e ) {
1003  global $wgShowExceptionDetails;
1004 
1005  if ( !$wgShowExceptionDetails ) {
1007  }
1008 
1010  "\nBacktrace:\n" .
1012  }
1013 
1025  public function makeModuleResponse( ResourceLoaderContext $context,
1026  array $modules, array $missing = []
1027  ) {
1028  $out = '';
1029  $states = [];
1030 
1031  if ( $modules === [] && $missing === [] ) {
1032  return <<<MESSAGE
1033 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1034  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1035  no modules were requested. Max made me put this here. */
1036 MESSAGE;
1037  }
1038 
1039  $image = $context->getImageObj();
1040  if ( $image ) {
1041  $data = $image->getImageData( $context );
1042  if ( $data === false ) {
1043  $data = '';
1044  $this->errors[] = 'Image generation failed';
1045  }
1046  return $data;
1047  }
1048 
1049  foreach ( $missing as $name ) {
1050  $states[$name] = 'missing';
1051  }
1052 
1053  // Generate output
1054  $isRaw = false;
1055 
1056  $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1057 
1058  foreach ( $modules as $name => $module ) {
1059  try {
1060  $content = $module->getModuleContent( $context );
1061  $implementKey = $name . '@' . $module->getVersionHash( $context );
1062  $strContent = '';
1063 
1064  if ( isset( $content['headers'] ) ) {
1065  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1066  }
1067 
1068  // Append output
1069  switch ( $context->getOnly() ) {
1070  case 'scripts':
1071  $scripts = $content['scripts'];
1072  if ( is_string( $scripts ) ) {
1073  // Load scripts raw...
1074  $strContent = $scripts;
1075  } elseif ( is_array( $scripts ) ) {
1076  // ...except when $scripts is an array of URLs or an associative array
1077  $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
1078  }
1079  break;
1080  case 'styles':
1081  $styles = $content['styles'];
1082  // We no longer separate into media, they are all combined now with
1083  // custom media type groups into @media .. {} sections as part of the css string.
1084  // Module returns either an empty array or a numerical array with css strings.
1085  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1086  break;
1087  default:
1088  $scripts = $content['scripts'] ?? '';
1089  if ( is_string( $scripts ) ) {
1090  if ( $name === 'site' || $name === 'user' ) {
1091  // Legacy scripts that run in the global scope without a closure.
1092  // mw.loader.implement will use globalEval if scripts is a string.
1093  // Minify manually here, because general response minification is
1094  // not effective due it being a string literal, not a function.
1095  if ( !$context->getDebug() ) {
1096  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1097  }
1098  } else {
1099  $scripts = new XmlJsCode( $scripts );
1100  }
1101  }
1102  $strContent = self::makeLoaderImplementScript(
1103  $implementKey,
1104  $scripts,
1105  $content['styles'] ?? [],
1106  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1107  $content['templates'] ?? []
1108  );
1109  break;
1110  }
1111 
1112  if ( !$context->getDebug() ) {
1113  $strContent = self::filter( $filter, $strContent );
1114  } else {
1115  // In debug mode, separate each response by a new line.
1116  // For example, between 'mw.loader.implement();' statements.
1117  $strContent = $this->ensureNewline( $strContent );
1118  }
1119 
1120  if ( $context->getOnly() === 'scripts' ) {
1121  // Use a linebreak between module scripts (T162719)
1122  $out .= $this->ensureNewline( $strContent );
1123  } else {
1124  $out .= $strContent;
1125  }
1126 
1127  } catch ( Exception $e ) {
1128  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1129 
1130  // Respond to client with error-state instead of module implementation
1131  $states[$name] = 'error';
1132  unset( $modules[$name] );
1133  }
1134  $isRaw |= $module->isRaw();
1135  }
1136 
1137  // Update module states
1138  if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
1139  if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
1140  // Set the state of modules loaded as only scripts to ready as
1141  // they don't have an mw.loader.implement wrapper that sets the state
1142  foreach ( $modules as $name => $module ) {
1143  $states[$name] = 'ready';
1144  }
1145  }
1146 
1147  // Set the state of modules we didn't respond to with mw.loader.implement
1148  if ( count( $states ) ) {
1149  $stateScript = self::makeLoaderStateScript( $states );
1150  if ( !$context->getDebug() ) {
1151  $stateScript = self::filter( 'minify-js', $stateScript );
1152  }
1153  // Use a linebreak between module script and state script (T162719)
1154  $out = $this->ensureNewline( $out ) . $stateScript;
1155  }
1156  } elseif ( $states ) {
1157  $this->errors[] = 'Problematic modules: '
1158  . self::encodeJsonForScript( $states );
1159  }
1160 
1161  return $out;
1162  }
1163 
1169  private function ensureNewline( $str ) {
1170  $end = substr( $str, -1 );
1171  if ( $end === false || $end === '' || $end === "\n" ) {
1172  return $str;
1173  }
1174  return $str . "\n";
1175  }
1176 
1183  public function getModulesByMessage( $messageKey ) {
1184  $moduleNames = [];
1185  foreach ( $this->getModuleNames() as $moduleName ) {
1186  $module = $this->getModule( $moduleName );
1187  if ( in_array( $messageKey, $module->getMessages() ) ) {
1188  $moduleNames[] = $moduleName;
1189  }
1190  }
1191  return $moduleNames;
1192  }
1193 
1211  protected static function makeLoaderImplementScript(
1212  $name, $scripts, $styles, $messages, $templates
1213  ) {
1214  if ( $scripts instanceof XmlJsCode ) {
1215  if ( $scripts->value === '' ) {
1216  $scripts = null;
1217  } elseif ( self::inDebugMode() ) {
1218  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1219  } else {
1220  $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
1221  }
1222  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1223  $files = $scripts['files'];
1224  foreach ( $files as $path => &$file ) {
1225  // $file is changed (by reference) from a descriptor array to the content of the file
1226  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1227  if ( $file['type'] === 'script' ) {
1228  // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1229  if ( self::inDebugMode() ) {
1230  $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
1231  } else {
1232  $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
1233  }
1234  } else {
1235  $file = $file['content'];
1236  }
1237  }
1238  $scripts = XmlJsCode::encodeObject( [
1239  'main' => $scripts['main'],
1240  'files' => XmlJsCode::encodeObject( $files, self::inDebugMode() )
1241  ], self::inDebugMode() );
1242  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1243  throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
1244  }
1245 
1246  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1247  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1248  // of "{}". Force them to objects.
1249  $module = [
1250  $name,
1251  $scripts,
1252  (object)$styles,
1253  (object)$messages,
1254  (object)$templates
1255  ];
1256  self::trimArray( $module );
1257 
1258  return Xml::encodeJsCall( 'mw.loader.implement', $module, self::inDebugMode() );
1259  }
1260 
1268  public static function makeMessageSetScript( $messages ) {
1269  return Xml::encodeJsCall(
1270  'mw.messages.set',
1271  [ (object)$messages ],
1272  self::inDebugMode()
1273  );
1274  }
1275 
1283  public static function makeCombinedStyles( array $stylePairs ) {
1284  $out = [];
1285  foreach ( $stylePairs as $media => $styles ) {
1286  // ResourceLoaderFileModule::getStyle can return the styles
1287  // as a string or an array of strings. This is to allow separation in
1288  // the front-end.
1289  $styles = (array)$styles;
1290  foreach ( $styles as $style ) {
1291  $style = trim( $style );
1292  // Don't output an empty "@media print { }" block (T42498)
1293  if ( $style !== '' ) {
1294  // Transform the media type based on request params and config
1295  // The way that this relies on $wgRequest to propagate request params is slightly evil
1296  $media = OutputPage::transformCssMedia( $media );
1297 
1298  if ( $media === '' || $media == 'all' ) {
1299  $out[] = $style;
1300  } elseif ( is_string( $media ) ) {
1301  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1302  }
1303  // else: skip
1304  }
1305  }
1306  }
1307  return $out;
1308  }
1309 
1319  public static function encodeJsonForScript( $data ) {
1320  // Keep output as small as possible by disabling needless escape modes
1321  // that PHP uses by default.
1322  // However, while most module scripts are only served on HTTP responses
1323  // for JavaScript, some modules can also be embedded in the HTML as inline
1324  // scripts. This, and the fact that we sometimes need to export strings
1325  // containing user-generated content and labels that may genuinely contain
1326  // a sequences like "</script>", we need to encode either '/' or '<'.
1327  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1328  // and allows URLs to mostly remain readable.
1329  $jsonFlags = JSON_UNESCAPED_SLASHES |
1330  JSON_UNESCAPED_UNICODE |
1331  JSON_HEX_TAG |
1332  JSON_HEX_AMP;
1333  if ( self::inDebugMode() ) {
1334  $jsonFlags |= JSON_PRETTY_PRINT;
1335  }
1336  return json_encode( $data, $jsonFlags );
1337  }
1338 
1353  public static function makeLoaderStateScript( $states, $state = null ) {
1354  if ( !is_array( $states ) ) {
1355  $states = [ $states => $state ];
1356  }
1357  return Xml::encodeJsCall(
1358  'mw.loader.state',
1359  [ $states ],
1360  self::inDebugMode()
1361  );
1362  }
1363 
1364  private static function isEmptyObject( stdClass $obj ) {
1365  foreach ( $obj as $key => $value ) {
1366  return false;
1367  }
1368  return true;
1369  }
1370 
1383  private static function trimArray( array &$array ) {
1384  $i = count( $array );
1385  while ( $i-- ) {
1386  if ( $array[$i] === null
1387  || $array[$i] === []
1388  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1389  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1390  ) {
1391  unset( $array[$i] );
1392  } else {
1393  break;
1394  }
1395  }
1396  }
1397 
1423  public static function makeLoaderRegisterScript( array $modules ) {
1424  // Optimisation: Transform dependency names into indexes when possible
1425  // to produce smaller output. They are expanded by mw.loader.register on
1426  // the other end using resolveIndexedDependencies().
1427  $index = [];
1428  foreach ( $modules as $i => &$module ) {
1429  // Build module name index
1430  $index[$module[0]] = $i;
1431  }
1432  foreach ( $modules as &$module ) {
1433  if ( isset( $module[2] ) ) {
1434  foreach ( $module[2] as &$dependency ) {
1435  if ( isset( $index[$dependency] ) ) {
1436  // Replace module name in dependency list with index
1437  $dependency = $index[$dependency];
1438  }
1439  }
1440  }
1441  }
1442 
1443  array_walk( $modules, [ self::class, 'trimArray' ] );
1444 
1445  return Xml::encodeJsCall(
1446  'mw.loader.register',
1447  [ $modules ],
1448  self::inDebugMode()
1449  );
1450  }
1451 
1466  public static function makeLoaderSourcesScript( $sources, $loadUrl = null ) {
1467  if ( !is_array( $sources ) ) {
1468  $sources = [ $sources => $loadUrl ];
1469  }
1470  return Xml::encodeJsCall(
1471  'mw.loader.addSource',
1472  [ $sources ],
1473  self::inDebugMode()
1474  );
1475  }
1476 
1483  public static function makeLoaderConditionalScript( $script ) {
1484  // Adds a function to lazy-created RLQ
1485  return '(window.RLQ=window.RLQ||[]).push(function(){' .
1486  trim( $script ) . '});';
1487  }
1488 
1497  public static function makeInlineCodeWithModule( $modules, $script ) {
1498  // Adds an array to lazy-created RLQ
1499  return '(window.RLQ=window.RLQ||[]).push(['
1500  . self::encodeJsonForScript( $modules ) . ','
1501  . 'function(){' . trim( $script ) . '}'
1502  . ']);';
1503  }
1504 
1516  public static function makeInlineScript( $script, $nonce = null ) {
1517  $js = self::makeLoaderConditionalScript( $script );
1518  $escNonce = '';
1519  if ( $nonce === null ) {
1520  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1521  } elseif ( $nonce !== false ) {
1522  // If it was false, CSP is disabled, so no nonce attribute.
1523  // Nonce should be only base64 characters, so should be safe,
1524  // but better to be safely escaped than sorry.
1525  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1526  }
1527 
1528  return new WrappedString(
1529  Html::inlineScript( $js, $nonce ),
1530  "<script$escNonce>(window.RLQ=window.RLQ||[]).push(function(){",
1531  '});</script>'
1532  );
1533  }
1534 
1543  public static function makeConfigSetScript( array $configuration ) {
1544  $js = Xml::encodeJsCall(
1545  'mw.config.set',
1546  [ $configuration ],
1547  self::inDebugMode()
1548  );
1549  if ( $js === false ) {
1550  $e = new Exception(
1551  'JSON serialization of config data failed. ' .
1552  'This usually means the config data is not valid UTF-8.'
1553  );
1555  $js = Xml::encodeJsCall( 'mw.log.error', [ $e->__toString() ] );
1556  }
1557  return $js;
1558  }
1559 
1573  public static function makePackedModulesString( $modules ) {
1574  $moduleMap = []; // [ prefix => [ suffixes ] ]
1575  foreach ( $modules as $module ) {
1576  $pos = strrpos( $module, '.' );
1577  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1578  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1579  $moduleMap[$prefix][] = $suffix;
1580  }
1581 
1582  $arr = [];
1583  foreach ( $moduleMap as $prefix => $suffixes ) {
1584  $p = $prefix === '' ? '' : $prefix . '.';
1585  $arr[] = $p . implode( ',', $suffixes );
1586  }
1587  return implode( '|', $arr );
1588  }
1589 
1601  public static function expandModuleNames( $modules ) {
1602  $retval = [];
1603  $exploded = explode( '|', $modules );
1604  foreach ( $exploded as $group ) {
1605  if ( strpos( $group, ',' ) === false ) {
1606  // This is not a set of modules in foo.bar,baz notation
1607  // but a single module
1608  $retval[] = $group;
1609  } else {
1610  // This is a set of modules in foo.bar,baz notation
1611  $pos = strrpos( $group, '.' );
1612  if ( $pos === false ) {
1613  // Prefixless modules, i.e. without dots
1614  $retval = array_merge( $retval, explode( ',', $group ) );
1615  } else {
1616  // We have a prefix and a bunch of suffixes
1617  $prefix = substr( $group, 0, $pos ); // 'foo'
1618  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1619  foreach ( $suffixes as $suffix ) {
1620  $retval[] = "$prefix.$suffix";
1621  }
1622  }
1623  }
1624  }
1625  return $retval;
1626  }
1627 
1633  public static function inDebugMode() {
1634  if ( self::$debugMode === null ) {
1636  self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
1637  $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
1638  );
1639  }
1640  return self::$debugMode;
1641  }
1642 
1653  public static function clearCache() {
1654  self::$debugMode = null;
1655  }
1656 
1666  public function createLoaderURL( $source, ResourceLoaderContext $context,
1667  $extraQuery = []
1668  ) {
1669  $query = self::createLoaderQuery( $context, $extraQuery );
1670  $script = $this->getLoadScript( $source );
1671 
1672  return wfAppendQuery( $script, $query );
1673  }
1674 
1684  protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
1685  return self::makeLoaderQuery(
1686  $context->getModules(),
1687  $context->getLanguage(),
1688  $context->getSkin(),
1689  $context->getUser(),
1690  $context->getVersion(),
1691  $context->getDebug(),
1692  $context->getOnly(),
1693  $context->getRequest()->getBool( 'printable' ),
1694  $context->getRequest()->getBool( 'handheld' ),
1695  $extraQuery
1696  );
1697  }
1698 
1716  public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
1717  $version = null, $debug = false, $only = null, $printable = false,
1718  $handheld = false, $extraQuery = []
1719  ) {
1720  $query = [
1721  'modules' => self::makePackedModulesString( $modules ),
1722  'lang' => $lang,
1723  'skin' => $skin,
1724  ];
1725  if ( $debug === true ) {
1726  $query['debug'] = 'true';
1727  }
1728  if ( $user !== null ) {
1729  $query['user'] = $user;
1730  }
1731  if ( $version !== null ) {
1732  $query['version'] = $version;
1733  }
1734  if ( $only !== null ) {
1735  $query['only'] = $only;
1736  }
1737  if ( $printable ) {
1738  $query['printable'] = 1;
1739  }
1740  if ( $handheld ) {
1741  $query['handheld'] = 1;
1742  }
1743  $query += $extraQuery;
1744 
1745  // Make queries uniform in order
1746  ksort( $query );
1747  return $query;
1748  }
1749 
1759  public static function isValidModuleName( $moduleName ) {
1760  return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName );
1761  }
1762 
1773  public function getLessCompiler( $vars = [] ) {
1774  global $IP;
1775  // When called from the installer, it is possible that a required PHP extension
1776  // is missing (at least for now; see T49564). If this is the case, throw an
1777  // exception (caught by the installer) to prevent a fatal error later on.
1778  if ( !class_exists( 'Less_Parser' ) ) {
1779  throw new MWException( 'MediaWiki requires the less.php parser' );
1780  }
1781 
1782  $parser = new Less_Parser;
1783  $parser->ModifyVars( $vars );
1784  $parser->SetImportDirs( [
1785  "$IP/resources/src/mediawiki.less/" => '',
1786  ] );
1787  $parser->SetOption( 'relativeUrls', false );
1788 
1789  return $parser;
1790  }
1791 
1799  public function getLessVars() {
1800  return [];
1801  }
1802 }
getTestModuleNames( $framework='all')
Get a list of test module names for one (or all) frameworks.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
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
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
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.
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:101
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:728
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...