MediaWiki
REL1_21
|
00001 <?php 00028 class ResourceLoaderFileModule extends ResourceLoaderModule { 00029 00030 /* Protected Members */ 00031 00033 protected $localBasePath = ''; 00035 protected $remoteBasePath = ''; 00043 protected $scripts = array(); 00051 protected $languageScripts = array(); 00059 protected $skinScripts = array(); 00067 protected $debugScripts = array(); 00075 protected $loaderScripts = array(); 00083 protected $styles = array(); 00091 protected $skinStyles = array(); 00099 protected $dependencies = array(); 00107 protected $messages = array(); 00109 protected $group; 00111 protected $position = 'bottom'; 00113 protected $debugRaw = true; 00115 protected $raw = false; 00116 protected $targets = array( 'desktop' ); 00117 00125 protected $modifiedTime = array(); 00133 protected $localFileRefs = array(); 00134 00135 /* Methods */ 00136 00189 public function __construct( $options = array(), $localBasePath = null, 00190 $remoteBasePath = null ) 00191 { 00192 global $IP, $wgScriptPath, $wgResourceBasePath; 00193 $this->localBasePath = $localBasePath === null ? $IP : $localBasePath; 00194 if ( $remoteBasePath !== null ) { 00195 $this->remoteBasePath = $remoteBasePath; 00196 } else { 00197 $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath; 00198 } 00199 00200 if ( isset( $options['remoteExtPath'] ) ) { 00201 global $wgExtensionAssetsPath; 00202 $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; 00203 } 00204 00205 foreach ( $options as $member => $option ) { 00206 switch ( $member ) { 00207 // Lists of file paths 00208 case 'scripts': 00209 case 'debugScripts': 00210 case 'loaderScripts': 00211 case 'styles': 00212 $this->{$member} = (array) $option; 00213 break; 00214 // Collated lists of file paths 00215 case 'languageScripts': 00216 case 'skinScripts': 00217 case 'skinStyles': 00218 if ( !is_array( $option ) ) { 00219 throw new MWException( 00220 "Invalid collated file path list error. " . 00221 "'$option' given, array expected." 00222 ); 00223 } 00224 foreach ( $option as $key => $value ) { 00225 if ( !is_string( $key ) ) { 00226 throw new MWException( 00227 "Invalid collated file path list key error. " . 00228 "'$key' given, string expected." 00229 ); 00230 } 00231 $this->{$member}[$key] = (array) $value; 00232 } 00233 break; 00234 // Lists of strings 00235 case 'dependencies': 00236 case 'messages': 00237 case 'targets': 00238 $this->{$member} = (array) $option; 00239 break; 00240 // Single strings 00241 case 'group': 00242 case 'position': 00243 case 'localBasePath': 00244 case 'remoteBasePath': 00245 $this->{$member} = (string) $option; 00246 break; 00247 // Single booleans 00248 case 'debugRaw': 00249 case 'raw': 00250 $this->{$member} = (bool) $option; 00251 break; 00252 } 00253 } 00254 // Make sure the remote base path is a complete valid URL, 00255 // but possibly protocol-relative to avoid cache pollution 00256 $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE ); 00257 } 00258 00265 public function getScript( ResourceLoaderContext $context ) { 00266 $files = $this->getScriptFiles( $context ); 00267 return $this->readScriptFiles( $files ); 00268 } 00269 00274 public function getScriptURLsForDebug( ResourceLoaderContext $context ) { 00275 $urls = array(); 00276 foreach ( $this->getScriptFiles( $context ) as $file ) { 00277 $urls[] = $this->getRemotePath( $file ); 00278 } 00279 return $urls; 00280 } 00281 00285 public function supportsURLLoading() { 00286 return $this->debugRaw; 00287 } 00288 00294 public function getLoaderScript() { 00295 if ( count( $this->loaderScripts ) == 0 ) { 00296 return false; 00297 } 00298 return $this->readScriptFiles( $this->loaderScripts ); 00299 } 00300 00307 public function getStyles( ResourceLoaderContext $context ) { 00308 $styles = $this->readStyleFiles( 00309 $this->getStyleFiles( $context ), 00310 $this->getFlip( $context ) 00311 ); 00312 // Collect referenced files 00313 $this->localFileRefs = array_unique( $this->localFileRefs ); 00314 // If the list has been modified since last time we cached it, update the cache 00315 try { 00316 if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) ) { 00317 $dbw = wfGetDB( DB_MASTER ); 00318 $dbw->replace( 'module_deps', 00319 array( array( 'md_module', 'md_skin' ) ), array( 00320 'md_module' => $this->getName(), 00321 'md_skin' => $context->getSkin(), 00322 'md_deps' => FormatJson::encode( $this->localFileRefs ), 00323 ) 00324 ); 00325 } 00326 } catch ( Exception $e ) { 00327 wfDebug( __METHOD__ . " failed to update DB: $e\n" ); 00328 } 00329 return $styles; 00330 } 00331 00336 public function getStyleURLsForDebug( ResourceLoaderContext $context ) { 00337 $urls = array(); 00338 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { 00339 $urls[$mediaType] = array(); 00340 foreach ( $list as $file ) { 00341 $urls[$mediaType][] = $this->getRemotePath( $file ); 00342 } 00343 } 00344 return $urls; 00345 } 00346 00352 public function getMessages() { 00353 return $this->messages; 00354 } 00355 00361 public function getGroup() { 00362 return $this->group; 00363 } 00364 00368 public function getPosition() { 00369 return $this->position; 00370 } 00371 00377 public function getDependencies() { 00378 return $this->dependencies; 00379 } 00380 00384 public function isRaw() { 00385 return $this->raw; 00386 } 00387 00402 public function getModifiedTime( ResourceLoaderContext $context ) { 00403 if ( isset( $this->modifiedTime[$context->getHash()] ) ) { 00404 return $this->modifiedTime[$context->getHash()]; 00405 } 00406 wfProfileIn( __METHOD__ ); 00407 00408 $files = array(); 00409 00410 // Flatten style files into $files 00411 $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' ); 00412 foreach ( $styles as $styleFiles ) { 00413 $files = array_merge( $files, $styleFiles ); 00414 } 00415 $skinFiles = self::tryForKey( 00416 self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ), 00417 $context->getSkin(), 00418 'default' 00419 ); 00420 foreach ( $skinFiles as $styleFiles ) { 00421 $files = array_merge( $files, $styleFiles ); 00422 } 00423 00424 // Final merge, this should result in a master list of dependent files 00425 $files = array_merge( 00426 $files, 00427 $this->scripts, 00428 $context->getDebug() ? $this->debugScripts : array(), 00429 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00430 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), 00431 $this->loaderScripts 00432 ); 00433 $files = array_map( array( $this, 'getLocalPath' ), $files ); 00434 // File deps need to be treated separately because they're already prefixed 00435 $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); 00436 00437 // If a module is nothing but a list of dependencies, we need to avoid 00438 // giving max() an empty array 00439 if ( count( $files ) === 0 ) { 00440 wfProfileOut( __METHOD__ ); 00441 return $this->modifiedTime[$context->getHash()] = 1; 00442 } 00443 00444 wfProfileIn( __METHOD__ . '-filemtime' ); 00445 $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); 00446 wfProfileOut( __METHOD__ . '-filemtime' ); 00447 $this->modifiedTime[$context->getHash()] = max( 00448 $filesMtime, 00449 $this->getMsgBlobMtime( $context->getLanguage() ) ); 00450 00451 wfProfileOut( __METHOD__ ); 00452 return $this->modifiedTime[$context->getHash()]; 00453 } 00454 00455 /* Protected Methods */ 00456 00461 protected function getLocalPath( $path ) { 00462 return "{$this->localBasePath}/$path"; 00463 } 00464 00469 protected function getRemotePath( $path ) { 00470 return "{$this->remoteBasePath}/$path"; 00471 } 00472 00482 protected static function collateFilePathListByOption( array $list, $option, $default ) { 00483 $collatedFiles = array(); 00484 foreach ( (array) $list as $key => $value ) { 00485 if ( is_int( $key ) ) { 00486 // File name as the value 00487 if ( !isset( $collatedFiles[$default] ) ) { 00488 $collatedFiles[$default] = array(); 00489 } 00490 $collatedFiles[$default][] = $value; 00491 } elseif ( is_array( $value ) ) { 00492 // File name as the key, options array as the value 00493 $optionValue = isset( $value[$option] ) ? $value[$option] : $default; 00494 if ( !isset( $collatedFiles[$optionValue] ) ) { 00495 $collatedFiles[$optionValue] = array(); 00496 } 00497 $collatedFiles[$optionValue][] = $key; 00498 } 00499 } 00500 return $collatedFiles; 00501 } 00502 00512 protected static function tryForKey( array $list, $key, $fallback = null ) { 00513 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { 00514 return $list[$key]; 00515 } elseif ( is_string( $fallback ) 00516 && isset( $list[$fallback] ) 00517 && is_array( $list[$fallback] ) ) 00518 { 00519 return $list[$fallback]; 00520 } 00521 return array(); 00522 } 00523 00530 protected function getScriptFiles( ResourceLoaderContext $context ) { 00531 $files = array_merge( 00532 $this->scripts, 00533 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00534 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) 00535 ); 00536 if ( $context->getDebug() ) { 00537 $files = array_merge( $files, $this->debugScripts ); 00538 } 00539 00540 return array_unique( $files ); 00541 } 00542 00549 protected function getStyleFiles( ResourceLoaderContext $context ) { 00550 return array_merge_recursive( 00551 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00552 self::collateFilePathListByOption( 00553 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' 00554 ) 00555 ); 00556 } 00557 00565 protected function readScriptFiles( array $scripts ) { 00566 global $wgResourceLoaderValidateStaticJS; 00567 if ( empty( $scripts ) ) { 00568 return ''; 00569 } 00570 $js = ''; 00571 foreach ( array_unique( $scripts ) as $fileName ) { 00572 $localPath = $this->getLocalPath( $fileName ); 00573 if ( !file_exists( $localPath ) ) { 00574 throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" ); 00575 } 00576 $contents = file_get_contents( $localPath ); 00577 if ( $wgResourceLoaderValidateStaticJS ) { 00578 // Static files don't really need to be checked as often; unlike 00579 // on-wiki module they shouldn't change unexpectedly without 00580 // admin interference. 00581 $contents = $this->validateScriptFile( $fileName, $contents ); 00582 } 00583 $js .= $contents . "\n"; 00584 } 00585 return $js; 00586 } 00587 00599 protected function readStyleFiles( array $styles, $flip ) { 00600 if ( empty( $styles ) ) { 00601 return array(); 00602 } 00603 foreach ( $styles as $media => $files ) { 00604 $uniqueFiles = array_unique( $files ); 00605 $styles[$media] = implode( 00606 "\n", 00607 array_map( 00608 array( $this, 'readStyleFile' ), 00609 $uniqueFiles, 00610 array_fill( 0, count( $uniqueFiles ), $flip ) 00611 ) 00612 ); 00613 } 00614 return $styles; 00615 } 00616 00628 protected function readStyleFile( $path, $flip ) { 00629 $localPath = $this->getLocalPath( $path ); 00630 if ( !file_exists( $localPath ) ) { 00631 $msg = __METHOD__ . ": style file not found: \"$localPath\""; 00632 wfDebugLog( 'resourceloader', $msg ); 00633 throw new MWException( $msg ); 00634 } 00635 $style = file_get_contents( $localPath ); 00636 if ( $flip ) { 00637 $style = CSSJanus::transform( $style, true, false ); 00638 } 00639 $dirname = dirname( $path ); 00640 if ( $dirname == '.' ) { 00641 // If $path doesn't have a directory component, don't prepend a dot 00642 $dirname = ''; 00643 } 00644 $dir = $this->getLocalPath( $dirname ); 00645 $remoteDir = $this->getRemotePath( $dirname ); 00646 // Get and register local file references 00647 $this->localFileRefs = array_merge( 00648 $this->localFileRefs, 00649 CSSMin::getLocalFileReferences( $style, $dir ) 00650 ); 00651 return CSSMin::remap( 00652 $style, $dir, $remoteDir, true 00653 ); 00654 } 00655 00661 public function getFlip( $context ) { 00662 return $context->getDirection() === 'rtl'; 00663 } 00664 00670 public function getTargets() { 00671 return $this->targets; 00672 } 00673 00674 }