MediaWiki
REL1_20
|
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; 00123 protected $modifiedTime = array(); 00131 protected $localFileRefs = array(); 00132 00133 /* Methods */ 00134 00186 public function __construct( $options = array(), $localBasePath = null, 00187 $remoteBasePath = null ) 00188 { 00189 global $IP, $wgScriptPath, $wgResourceBasePath; 00190 $this->localBasePath = $localBasePath === null ? $IP : $localBasePath; 00191 if ( $remoteBasePath !== null ) { 00192 $this->remoteBasePath = $remoteBasePath; 00193 } else { 00194 $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath; 00195 } 00196 00197 if ( isset( $options['remoteExtPath'] ) ) { 00198 global $wgExtensionAssetsPath; 00199 $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; 00200 } 00201 00202 foreach ( $options as $member => $option ) { 00203 switch ( $member ) { 00204 // Lists of file paths 00205 case 'scripts': 00206 case 'debugScripts': 00207 case 'loaderScripts': 00208 case 'styles': 00209 $this->{$member} = (array) $option; 00210 break; 00211 // Collated lists of file paths 00212 case 'languageScripts': 00213 case 'skinScripts': 00214 case 'skinStyles': 00215 if ( !is_array( $option ) ) { 00216 throw new MWException( 00217 "Invalid collated file path list error. " . 00218 "'$option' given, array expected." 00219 ); 00220 } 00221 foreach ( $option as $key => $value ) { 00222 if ( !is_string( $key ) ) { 00223 throw new MWException( 00224 "Invalid collated file path list key error. " . 00225 "'$key' given, string expected." 00226 ); 00227 } 00228 $this->{$member}[$key] = (array) $value; 00229 } 00230 break; 00231 // Lists of strings 00232 case 'dependencies': 00233 case 'messages': 00234 $this->{$member} = (array) $option; 00235 break; 00236 // Single strings 00237 case 'group': 00238 case 'position': 00239 case 'localBasePath': 00240 case 'remoteBasePath': 00241 $this->{$member} = (string) $option; 00242 break; 00243 // Single booleans 00244 case 'debugRaw': 00245 case 'raw': 00246 $this->{$member} = (bool) $option; 00247 break; 00248 } 00249 } 00250 // Make sure the remote base path is a complete valid URL, 00251 // but possibly protocol-relative to avoid cache pollution 00252 $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE ); 00253 } 00254 00261 public function getScript( ResourceLoaderContext $context ) { 00262 $files = $this->getScriptFiles( $context ); 00263 return $this->readScriptFiles( $files ); 00264 } 00265 00270 public function getScriptURLsForDebug( ResourceLoaderContext $context ) { 00271 $urls = array(); 00272 foreach ( $this->getScriptFiles( $context ) as $file ) { 00273 $urls[] = $this->getRemotePath( $file ); 00274 } 00275 return $urls; 00276 } 00277 00281 public function supportsURLLoading() { 00282 return $this->debugRaw; 00283 } 00284 00290 public function getLoaderScript() { 00291 if ( count( $this->loaderScripts ) == 0 ) { 00292 return false; 00293 } 00294 return $this->readScriptFiles( $this->loaderScripts ); 00295 } 00296 00303 public function getStyles( ResourceLoaderContext $context ) { 00304 $styles = $this->readStyleFiles( 00305 $this->getStyleFiles( $context ), 00306 $this->getFlip( $context ) 00307 ); 00308 // Collect referenced files 00309 $this->localFileRefs = array_unique( $this->localFileRefs ); 00310 // If the list has been modified since last time we cached it, update the cache 00311 try { 00312 if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) ) { 00313 $dbw = wfGetDB( DB_MASTER ); 00314 $dbw->replace( 'module_deps', 00315 array( array( 'md_module', 'md_skin' ) ), array( 00316 'md_module' => $this->getName(), 00317 'md_skin' => $context->getSkin(), 00318 'md_deps' => FormatJson::encode( $this->localFileRefs ), 00319 ) 00320 ); 00321 } 00322 } catch ( Exception $e ) { 00323 wfDebug( __METHOD__ . " failed to update DB: $e\n" ); 00324 } 00325 return $styles; 00326 } 00327 00332 public function getStyleURLsForDebug( ResourceLoaderContext $context ) { 00333 $urls = array(); 00334 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { 00335 $urls[$mediaType] = array(); 00336 foreach ( $list as $file ) { 00337 $urls[$mediaType][] = $this->getRemotePath( $file ); 00338 } 00339 } 00340 return $urls; 00341 } 00342 00348 public function getMessages() { 00349 return $this->messages; 00350 } 00351 00357 public function getGroup() { 00358 return $this->group; 00359 } 00360 00364 public function getPosition() { 00365 return $this->position; 00366 } 00367 00373 public function getDependencies() { 00374 return $this->dependencies; 00375 } 00376 00380 public function isRaw() { 00381 return $this->raw; 00382 } 00383 00398 public function getModifiedTime( ResourceLoaderContext $context ) { 00399 if ( isset( $this->modifiedTime[$context->getHash()] ) ) { 00400 return $this->modifiedTime[$context->getHash()]; 00401 } 00402 wfProfileIn( __METHOD__ ); 00403 00404 $files = array(); 00405 00406 // Flatten style files into $files 00407 $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' ); 00408 foreach ( $styles as $styleFiles ) { 00409 $files = array_merge( $files, $styleFiles ); 00410 } 00411 $skinFiles = self::tryForKey( 00412 self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ), 00413 $context->getSkin(), 00414 'default' 00415 ); 00416 foreach ( $skinFiles as $styleFiles ) { 00417 $files = array_merge( $files, $styleFiles ); 00418 } 00419 00420 // Final merge, this should result in a master list of dependent files 00421 $files = array_merge( 00422 $files, 00423 $this->scripts, 00424 $context->getDebug() ? $this->debugScripts : array(), 00425 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00426 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), 00427 $this->loaderScripts 00428 ); 00429 $files = array_map( array( $this, 'getLocalPath' ), $files ); 00430 // File deps need to be treated separately because they're already prefixed 00431 $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); 00432 00433 // If a module is nothing but a list of dependencies, we need to avoid 00434 // giving max() an empty array 00435 if ( count( $files ) === 0 ) { 00436 wfProfileOut( __METHOD__ ); 00437 return $this->modifiedTime[$context->getHash()] = 1; 00438 } 00439 00440 wfProfileIn( __METHOD__.'-filemtime' ); 00441 $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); 00442 wfProfileOut( __METHOD__.'-filemtime' ); 00443 $this->modifiedTime[$context->getHash()] = max( 00444 $filesMtime, 00445 $this->getMsgBlobMtime( $context->getLanguage() ) ); 00446 00447 wfProfileOut( __METHOD__ ); 00448 return $this->modifiedTime[$context->getHash()]; 00449 } 00450 00451 /* Protected Methods */ 00452 00457 protected function getLocalPath( $path ) { 00458 return "{$this->localBasePath}/$path"; 00459 } 00460 00465 protected function getRemotePath( $path ) { 00466 return "{$this->remoteBasePath}/$path"; 00467 } 00468 00478 protected static function collateFilePathListByOption( array $list, $option, $default ) { 00479 $collatedFiles = array(); 00480 foreach ( (array) $list as $key => $value ) { 00481 if ( is_int( $key ) ) { 00482 // File name as the value 00483 if ( !isset( $collatedFiles[$default] ) ) { 00484 $collatedFiles[$default] = array(); 00485 } 00486 $collatedFiles[$default][] = $value; 00487 } elseif ( is_array( $value ) ) { 00488 // File name as the key, options array as the value 00489 $optionValue = isset( $value[$option] ) ? $value[$option] : $default; 00490 if ( !isset( $collatedFiles[$optionValue] ) ) { 00491 $collatedFiles[$optionValue] = array(); 00492 } 00493 $collatedFiles[$optionValue][] = $key; 00494 } 00495 } 00496 return $collatedFiles; 00497 } 00498 00508 protected static function tryForKey( array $list, $key, $fallback = null ) { 00509 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { 00510 return $list[$key]; 00511 } elseif ( is_string( $fallback ) 00512 && isset( $list[$fallback] ) 00513 && is_array( $list[$fallback] ) ) 00514 { 00515 return $list[$fallback]; 00516 } 00517 return array(); 00518 } 00519 00526 protected function getScriptFiles( ResourceLoaderContext $context ) { 00527 $files = array_merge( 00528 $this->scripts, 00529 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00530 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) 00531 ); 00532 if ( $context->getDebug() ) { 00533 $files = array_merge( $files, $this->debugScripts ); 00534 } 00535 return $files; 00536 } 00537 00544 protected function getStyleFiles( ResourceLoaderContext $context ) { 00545 return array_merge_recursive( 00546 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00547 self::collateFilePathListByOption( 00548 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' 00549 ) 00550 ); 00551 } 00552 00559 protected function readScriptFiles( array $scripts ) { 00560 global $wgResourceLoaderValidateStaticJS; 00561 if ( empty( $scripts ) ) { 00562 return ''; 00563 } 00564 $js = ''; 00565 foreach ( array_unique( $scripts ) as $fileName ) { 00566 $localPath = $this->getLocalPath( $fileName ); 00567 if ( !file_exists( $localPath ) ) { 00568 throw new MWException( __METHOD__.": script file not found: \"$localPath\"" ); 00569 } 00570 $contents = file_get_contents( $localPath ); 00571 if ( $wgResourceLoaderValidateStaticJS ) { 00572 // Static files don't really need to be checked as often; unlike 00573 // on-wiki module they shouldn't change unexpectedly without 00574 // admin interference. 00575 $contents = $this->validateScriptFile( $fileName, $contents ); 00576 } 00577 $js .= $contents . "\n"; 00578 } 00579 return $js; 00580 } 00581 00593 protected function readStyleFiles( array $styles, $flip ) { 00594 if ( empty( $styles ) ) { 00595 return array(); 00596 } 00597 foreach ( $styles as $media => $files ) { 00598 $uniqueFiles = array_unique( $files ); 00599 $styles[$media] = implode( 00600 "\n", 00601 array_map( 00602 array( $this, 'readStyleFile' ), 00603 $uniqueFiles, 00604 array_fill( 0, count( $uniqueFiles ), $flip ) 00605 ) 00606 ); 00607 } 00608 return $styles; 00609 } 00610 00622 protected function readStyleFile( $path, $flip ) { 00623 $localPath = $this->getLocalPath( $path ); 00624 if ( !file_exists( $localPath ) ) { 00625 throw new MWException( __METHOD__.": style file not found: \"$localPath\"" ); 00626 } 00627 $style = file_get_contents( $localPath ); 00628 if ( $flip ) { 00629 $style = CSSJanus::transform( $style, true, false ); 00630 } 00631 $dirname = dirname( $path ); 00632 if ( $dirname == '.' ) { 00633 // If $path doesn't have a directory component, don't prepend a dot 00634 $dirname = ''; 00635 } 00636 $dir = $this->getLocalPath( $dirname ); 00637 $remoteDir = $this->getRemotePath( $dirname ); 00638 // Get and register local file references 00639 $this->localFileRefs = array_merge( 00640 $this->localFileRefs, 00641 CSSMin::getLocalFileReferences( $style, $dir ) 00642 ); 00643 return CSSMin::remap( 00644 $style, $dir, $remoteDir, true 00645 ); 00646 } 00647 00654 protected static function safeFilemtime( $filename ) { 00655 if ( file_exists( $filename ) ) { 00656 return filemtime( $filename ); 00657 } else { 00658 // We only ever map this function on an array if we're gonna call max() after, 00659 // so return our standard minimum timestamps here. This is 1, not 0, because 00660 // wfTimestamp(0) == NOW 00661 return 1; 00662 } 00663 } 00664 00670 public function getFlip( $context ) { 00671 return $context->getDirection() === 'rtl'; 00672 } 00673 }