MediaWiki
REL1_23
|
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 00122 protected $hasGeneratedStyles = false; 00123 00131 protected $modifiedTime = array(); 00139 protected $localFileRefs = array(); 00140 00141 /* Methods */ 00142 00195 public function __construct( $options = array(), $localBasePath = null, 00196 $remoteBasePath = null 00197 ) { 00198 global $IP, $wgScriptPath, $wgResourceBasePath; 00199 $this->localBasePath = $localBasePath === null ? $IP : $localBasePath; 00200 if ( $remoteBasePath !== null ) { 00201 $this->remoteBasePath = $remoteBasePath; 00202 } else { 00203 $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath; 00204 } 00205 00206 if ( isset( $options['remoteExtPath'] ) ) { 00207 global $wgExtensionAssetsPath; 00208 $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; 00209 } 00210 00211 foreach ( $options as $member => $option ) { 00212 switch ( $member ) { 00213 // Lists of file paths 00214 case 'scripts': 00215 case 'debugScripts': 00216 case 'loaderScripts': 00217 case 'styles': 00218 $this->{$member} = (array)$option; 00219 break; 00220 // Collated lists of file paths 00221 case 'languageScripts': 00222 case 'skinScripts': 00223 case 'skinStyles': 00224 if ( !is_array( $option ) ) { 00225 throw new MWException( 00226 "Invalid collated file path list error. " . 00227 "'$option' given, array expected." 00228 ); 00229 } 00230 foreach ( $option as $key => $value ) { 00231 if ( !is_string( $key ) ) { 00232 throw new MWException( 00233 "Invalid collated file path list key error. " . 00234 "'$key' given, string expected." 00235 ); 00236 } 00237 $this->{$member}[$key] = (array)$value; 00238 } 00239 break; 00240 // Lists of strings 00241 case 'dependencies': 00242 case 'messages': 00243 case 'targets': 00244 // Normalise 00245 $option = array_values( array_unique( (array)$option ) ); 00246 sort( $option ); 00247 00248 $this->{$member} = $option; 00249 break; 00250 // Single strings 00251 case 'group': 00252 case 'position': 00253 case 'localBasePath': 00254 case 'remoteBasePath': 00255 $this->{$member} = (string)$option; 00256 break; 00257 // Single booleans 00258 case 'debugRaw': 00259 case 'raw': 00260 $this->{$member} = (bool)$option; 00261 break; 00262 } 00263 } 00264 // Make sure the remote base path is a complete valid URL, 00265 // but possibly protocol-relative to avoid cache pollution 00266 $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE ); 00267 } 00268 00275 public function getScript( ResourceLoaderContext $context ) { 00276 $files = $this->getScriptFiles( $context ); 00277 return $this->readScriptFiles( $files ); 00278 } 00279 00284 public function getScriptURLsForDebug( ResourceLoaderContext $context ) { 00285 $urls = array(); 00286 foreach ( $this->getScriptFiles( $context ) as $file ) { 00287 $urls[] = $this->getRemotePath( $file ); 00288 } 00289 return $urls; 00290 } 00291 00295 public function supportsURLLoading() { 00296 return $this->debugRaw; 00297 } 00298 00304 public function getLoaderScript() { 00305 if ( count( $this->loaderScripts ) == 0 ) { 00306 return false; 00307 } 00308 return $this->readScriptFiles( $this->loaderScripts ); 00309 } 00310 00317 public function getStyles( ResourceLoaderContext $context ) { 00318 $styles = $this->readStyleFiles( 00319 $this->getStyleFiles( $context ), 00320 $this->getFlip( $context ) 00321 ); 00322 // Collect referenced files 00323 $this->localFileRefs = array_unique( $this->localFileRefs ); 00324 // If the list has been modified since last time we cached it, update the cache 00325 try { 00326 if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) ) { 00327 $dbw = wfGetDB( DB_MASTER ); 00328 $dbw->replace( 'module_deps', 00329 array( array( 'md_module', 'md_skin' ) ), array( 00330 'md_module' => $this->getName(), 00331 'md_skin' => $context->getSkin(), 00332 'md_deps' => FormatJson::encode( $this->localFileRefs ), 00333 ) 00334 ); 00335 } 00336 } catch ( Exception $e ) { 00337 wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" ); 00338 } 00339 return $styles; 00340 } 00341 00346 public function getStyleURLsForDebug( ResourceLoaderContext $context ) { 00347 if ( $this->hasGeneratedStyles ) { 00348 // Do the default behaviour of returning a url back to load.php 00349 // but with only=styles. 00350 return parent::getStyleURLsForDebug( $context ); 00351 } 00352 // Our module consists entirely of real css files, 00353 // in debug mode we can load those directly. 00354 $urls = array(); 00355 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { 00356 $urls[$mediaType] = array(); 00357 foreach ( $list as $file ) { 00358 $urls[$mediaType][] = $this->getRemotePath( $file ); 00359 } 00360 } 00361 return $urls; 00362 } 00363 00369 public function getMessages() { 00370 return $this->messages; 00371 } 00372 00378 public function getGroup() { 00379 return $this->group; 00380 } 00381 00385 public function getPosition() { 00386 return $this->position; 00387 } 00388 00394 public function getDependencies() { 00395 return $this->dependencies; 00396 } 00397 00401 public function isRaw() { 00402 return $this->raw; 00403 } 00404 00419 public function getModifiedTime( ResourceLoaderContext $context ) { 00420 if ( isset( $this->modifiedTime[$context->getHash()] ) ) { 00421 return $this->modifiedTime[$context->getHash()]; 00422 } 00423 wfProfileIn( __METHOD__ ); 00424 00425 $files = array(); 00426 00427 // Flatten style files into $files 00428 $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' ); 00429 foreach ( $styles as $styleFiles ) { 00430 $files = array_merge( $files, $styleFiles ); 00431 } 00432 00433 $skinFiles = self::collateFilePathListByOption( 00434 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 00435 'media', 00436 'all' 00437 ); 00438 foreach ( $skinFiles as $styleFiles ) { 00439 $files = array_merge( $files, $styleFiles ); 00440 } 00441 00442 // Final merge, this should result in a master list of dependent files 00443 $files = array_merge( 00444 $files, 00445 $this->scripts, 00446 $context->getDebug() ? $this->debugScripts : array(), 00447 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00448 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), 00449 $this->loaderScripts 00450 ); 00451 $files = array_map( array( $this, 'getLocalPath' ), $files ); 00452 // File deps need to be treated separately because they're already prefixed 00453 $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); 00454 00455 // If a module is nothing but a list of dependencies, we need to avoid 00456 // giving max() an empty array 00457 if ( count( $files ) === 0 ) { 00458 $this->modifiedTime[$context->getHash()] = 1; 00459 wfProfileOut( __METHOD__ ); 00460 return $this->modifiedTime[$context->getHash()]; 00461 } 00462 00463 wfProfileIn( __METHOD__ . '-filemtime' ); 00464 $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); 00465 wfProfileOut( __METHOD__ . '-filemtime' ); 00466 00467 $this->modifiedTime[$context->getHash()] = max( 00468 $filesMtime, 00469 $this->getMsgBlobMtime( $context->getLanguage() ), 00470 $this->getDefinitionMtime( $context ) 00471 ); 00472 00473 wfProfileOut( __METHOD__ ); 00474 return $this->modifiedTime[$context->getHash()]; 00475 } 00476 00482 public function getDefinitionSummary( ResourceLoaderContext $context ) { 00483 $summary = array( 00484 'class' => get_class( $this ), 00485 ); 00486 foreach ( array( 00487 'scripts', 00488 'debugScripts', 00489 'loaderScripts', 00490 'styles', 00491 'languageScripts', 00492 'skinScripts', 00493 'skinStyles', 00494 'dependencies', 00495 'messages', 00496 'targets', 00497 'group', 00498 'position', 00499 'localBasePath', 00500 'remoteBasePath', 00501 'debugRaw', 00502 'raw', 00503 ) as $member ) { 00504 $summary[$member] = $this->{$member}; 00505 }; 00506 return $summary; 00507 } 00508 00509 /* Protected Methods */ 00510 00515 protected function getLocalPath( $path ) { 00516 return "{$this->localBasePath}/$path"; 00517 } 00518 00523 protected function getRemotePath( $path ) { 00524 return "{$this->remoteBasePath}/$path"; 00525 } 00526 00534 public function getStyleSheetLang( $path ) { 00535 return preg_match( '/\.less$/i', $path ) ? 'less' : 'css'; 00536 } 00537 00547 protected static function collateFilePathListByOption( array $list, $option, $default ) { 00548 $collatedFiles = array(); 00549 foreach ( (array)$list as $key => $value ) { 00550 if ( is_int( $key ) ) { 00551 // File name as the value 00552 if ( !isset( $collatedFiles[$default] ) ) { 00553 $collatedFiles[$default] = array(); 00554 } 00555 $collatedFiles[$default][] = $value; 00556 } elseif ( is_array( $value ) ) { 00557 // File name as the key, options array as the value 00558 $optionValue = isset( $value[$option] ) ? $value[$option] : $default; 00559 if ( !isset( $collatedFiles[$optionValue] ) ) { 00560 $collatedFiles[$optionValue] = array(); 00561 } 00562 $collatedFiles[$optionValue][] = $key; 00563 } 00564 } 00565 return $collatedFiles; 00566 } 00567 00577 protected static function tryForKey( array $list, $key, $fallback = null ) { 00578 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { 00579 return $list[$key]; 00580 } elseif ( is_string( $fallback ) 00581 && isset( $list[$fallback] ) 00582 && is_array( $list[$fallback] ) 00583 ) { 00584 return $list[$fallback]; 00585 } 00586 return array(); 00587 } 00588 00595 protected function getScriptFiles( ResourceLoaderContext $context ) { 00596 $files = array_merge( 00597 $this->scripts, 00598 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00599 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) 00600 ); 00601 if ( $context->getDebug() ) { 00602 $files = array_merge( $files, $this->debugScripts ); 00603 } 00604 00605 return array_unique( $files ); 00606 } 00607 00614 protected function getStyleFiles( ResourceLoaderContext $context ) { 00615 return array_merge_recursive( 00616 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00617 self::collateFilePathListByOption( 00618 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 00619 'media', 00620 'all' 00621 ) 00622 ); 00623 } 00624 00629 public function getAllStyleFiles() { 00630 $files = array(); 00631 foreach ( (array)$this->styles as $key => $value ) { 00632 if ( is_array( $value ) ) { 00633 $path = $key; 00634 } else { 00635 $path = $value; 00636 } 00637 $files[] = $this->getLocalPath( $path ); 00638 } 00639 return $files; 00640 } 00641 00649 protected function readScriptFiles( array $scripts ) { 00650 global $wgResourceLoaderValidateStaticJS; 00651 if ( empty( $scripts ) ) { 00652 return ''; 00653 } 00654 $js = ''; 00655 foreach ( array_unique( $scripts ) as $fileName ) { 00656 $localPath = $this->getLocalPath( $fileName ); 00657 if ( !file_exists( $localPath ) ) { 00658 throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" ); 00659 } 00660 $contents = file_get_contents( $localPath ); 00661 if ( $wgResourceLoaderValidateStaticJS ) { 00662 // Static files don't really need to be checked as often; unlike 00663 // on-wiki module they shouldn't change unexpectedly without 00664 // admin interference. 00665 $contents = $this->validateScriptFile( $fileName, $contents ); 00666 } 00667 $js .= $contents . "\n"; 00668 } 00669 return $js; 00670 } 00671 00684 protected function readStyleFiles( array $styles, $flip ) { 00685 if ( empty( $styles ) ) { 00686 return array(); 00687 } 00688 foreach ( $styles as $media => $files ) { 00689 $uniqueFiles = array_unique( $files ); 00690 $styleFiles = array(); 00691 foreach ( $uniqueFiles as $file ) { 00692 $styleFiles[] = $this->readStyleFile( $file, $flip ); 00693 } 00694 $styles[$media] = implode( "\n", $styleFiles ); 00695 } 00696 return $styles; 00697 } 00698 00710 protected function readStyleFile( $path, $flip ) { 00711 $localPath = $this->getLocalPath( $path ); 00712 if ( !file_exists( $localPath ) ) { 00713 $msg = __METHOD__ . ": style file not found: \"$localPath\""; 00714 wfDebugLog( 'resourceloader', $msg ); 00715 throw new MWException( $msg ); 00716 } 00717 00718 if ( $this->getStyleSheetLang( $path ) === 'less' ) { 00719 $style = $this->compileLESSFile( $localPath ); 00720 $this->hasGeneratedStyles = true; 00721 } else { 00722 $style = file_get_contents( $localPath ); 00723 } 00724 00725 if ( $flip ) { 00726 $style = CSSJanus::transform( $style, true, false ); 00727 } 00728 $dirname = dirname( $path ); 00729 if ( $dirname == '.' ) { 00730 // If $path doesn't have a directory component, don't prepend a dot 00731 $dirname = ''; 00732 } 00733 $dir = $this->getLocalPath( $dirname ); 00734 $remoteDir = $this->getRemotePath( $dirname ); 00735 // Get and register local file references 00736 $this->localFileRefs = array_merge( 00737 $this->localFileRefs, 00738 CSSMin::getLocalFileReferences( $style, $dir ) 00739 ); 00740 return CSSMin::remap( 00741 $style, $dir, $remoteDir, true 00742 ); 00743 } 00744 00750 public function getFlip( $context ) { 00751 return $context->getDirection() === 'rtl'; 00752 } 00753 00759 public function getTargets() { 00760 return $this->targets; 00761 } 00762 00773 protected static function getLESSCacheKey( $fileName ) { 00774 $vars = json_encode( ResourceLoader::getLESSVars() ); 00775 $hash = md5( $fileName . $vars ); 00776 return wfMemcKey( 'resourceloader', 'less', $hash ); 00777 } 00778 00794 protected function compileLESSFile( $fileName ) { 00795 $key = self::getLESSCacheKey( $fileName ); 00796 $cache = wfGetCache( CACHE_ANYTHING ); 00797 00798 // The input to lessc. Either an associative array representing the 00799 // cached results of a previous compilation, or the string file name if 00800 // no cache result exists. 00801 $source = $cache->get( $key ); 00802 if ( !is_array( $source ) || !isset( $source['root'] ) ) { 00803 $source = $fileName; 00804 } 00805 00806 $compiler = ResourceLoader::getLessCompiler(); 00807 $result = null; 00808 00809 $result = $compiler->cachedCompile( $source ); 00810 00811 if ( !is_array( $result ) ) { 00812 throw new MWException( 'LESS compiler result has type ' . gettype( $result ) . '; array expected.' ); 00813 } 00814 00815 $this->localFileRefs += array_keys( $result['files'] ); 00816 $cache->set( $key, $result ); 00817 return $result['compiled']; 00818 } 00819 }