MediaWiki
master
|
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 if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) && !wfReadOnly() ) { 00316 $dbw = wfGetDB( DB_MASTER ); 00317 $dbw->replace( 'module_deps', 00318 array( array( 'md_module', 'md_skin' ) ), array( 00319 'md_module' => $this->getName(), 00320 'md_skin' => $context->getSkin(), 00321 'md_deps' => FormatJson::encode( $this->localFileRefs ), 00322 ) 00323 ); 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 00536 return array_unique( $files ); 00537 } 00538 00545 protected function getStyleFiles( ResourceLoaderContext $context ) { 00546 return array_merge_recursive( 00547 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00548 self::collateFilePathListByOption( 00549 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' 00550 ) 00551 ); 00552 } 00553 00561 protected function readScriptFiles( array $scripts ) { 00562 global $wgResourceLoaderValidateStaticJS; 00563 if ( empty( $scripts ) ) { 00564 return ''; 00565 } 00566 $js = ''; 00567 foreach ( array_unique( $scripts ) as $fileName ) { 00568 $localPath = $this->getLocalPath( $fileName ); 00569 if ( !file_exists( $localPath ) ) { 00570 throw new MWException( __METHOD__.": script file not found: \"$localPath\"" ); 00571 } 00572 $contents = file_get_contents( $localPath ); 00573 if ( $wgResourceLoaderValidateStaticJS ) { 00574 // Static files don't really need to be checked as often; unlike 00575 // on-wiki module they shouldn't change unexpectedly without 00576 // admin interference. 00577 $contents = $this->validateScriptFile( $fileName, $contents ); 00578 } 00579 $js .= $contents . "\n"; 00580 } 00581 return $js; 00582 } 00583 00595 protected function readStyleFiles( array $styles, $flip ) { 00596 if ( empty( $styles ) ) { 00597 return array(); 00598 } 00599 foreach ( $styles as $media => $files ) { 00600 $uniqueFiles = array_unique( $files ); 00601 $styles[$media] = implode( 00602 "\n", 00603 array_map( 00604 array( $this, 'readStyleFile' ), 00605 $uniqueFiles, 00606 array_fill( 0, count( $uniqueFiles ), $flip ) 00607 ) 00608 ); 00609 } 00610 return $styles; 00611 } 00612 00624 protected function readStyleFile( $path, $flip ) { 00625 $localPath = $this->getLocalPath( $path ); 00626 if ( !file_exists( $localPath ) ) { 00627 $msg = __METHOD__.": style file not found: \"$localPath\""; 00628 wfDebugLog( 'resourceloader', $msg ); 00629 throw new MWException( $msg ); 00630 } 00631 $style = file_get_contents( $localPath ); 00632 if ( $flip ) { 00633 $style = CSSJanus::transform( $style, true, false ); 00634 } 00635 $dirname = dirname( $path ); 00636 if ( $dirname == '.' ) { 00637 // If $path doesn't have a directory component, don't prepend a dot 00638 $dirname = ''; 00639 } 00640 $dir = $this->getLocalPath( $dirname ); 00641 $remoteDir = $this->getRemotePath( $dirname ); 00642 // Get and register local file references 00643 $this->localFileRefs = array_merge( 00644 $this->localFileRefs, 00645 CSSMin::getLocalFileReferences( $style, $dir ) 00646 ); 00647 return CSSMin::remap( 00648 $style, $dir, $remoteDir, true 00649 ); 00650 } 00651 00658 protected static function safeFilemtime( $filename ) { 00659 if ( file_exists( $filename ) ) { 00660 return filemtime( $filename ); 00661 } else { 00662 // We only ever map this function on an array if we're gonna call max() after, 00663 // so return our standard minimum timestamps here. This is 1, not 0, because 00664 // wfTimestamp(0) == NOW 00665 return 1; 00666 } 00667 } 00668 00674 public function getFlip( $context ) { 00675 return $context->getDirection() === 'rtl'; 00676 } 00677 00683 public function getTargets() { 00684 return $this->targets; 00685 } 00686 00687 }