MediaWiki
REL1_24
|
00001 <?php 00028 class ResourceLoaderFileModule extends ResourceLoaderModule { 00029 /* Protected Members */ 00030 00032 protected $localBasePath = ''; 00033 00035 protected $remoteBasePath = ''; 00036 00044 protected $scripts = array(); 00045 00053 protected $languageScripts = array(); 00054 00062 protected $skinScripts = array(); 00063 00071 protected $debugScripts = array(); 00072 00080 protected $loaderScripts = array(); 00081 00089 protected $styles = array(); 00090 00098 protected $skinStyles = array(); 00099 00107 protected $dependencies = array(); 00108 00112 protected $skipFunction = null; 00113 00121 protected $messages = array(); 00122 00124 protected $group; 00125 00127 protected $position = 'bottom'; 00128 00130 protected $debugRaw = true; 00131 00133 protected $raw = false; 00134 00135 protected $targets = array( 'desktop' ); 00136 00141 protected $hasGeneratedStyles = false; 00142 00150 protected $modifiedTime = array(); 00151 00159 protected $localFileRefs = array(); 00160 00161 /* Methods */ 00162 00221 public function __construct( 00222 $options = array(), 00223 $localBasePath = null, 00224 $remoteBasePath = null 00225 ) { 00226 // localBasePath and remoteBasePath both have unbelievably long fallback chains 00227 // and need to be handled separately. 00228 list( $this->localBasePath, $this->remoteBasePath ) = 00229 self::extractBasePaths( $options, $localBasePath, $remoteBasePath ); 00230 00231 // Extract, validate and normalise remaining options 00232 foreach ( $options as $member => $option ) { 00233 switch ( $member ) { 00234 // Lists of file paths 00235 case 'scripts': 00236 case 'debugScripts': 00237 case 'loaderScripts': 00238 case 'styles': 00239 $this->{$member} = (array)$option; 00240 break; 00241 // Collated lists of file paths 00242 case 'languageScripts': 00243 case 'skinScripts': 00244 case 'skinStyles': 00245 if ( !is_array( $option ) ) { 00246 throw new MWException( 00247 "Invalid collated file path list error. " . 00248 "'$option' given, array expected." 00249 ); 00250 } 00251 foreach ( $option as $key => $value ) { 00252 if ( !is_string( $key ) ) { 00253 throw new MWException( 00254 "Invalid collated file path list key error. " . 00255 "'$key' given, string expected." 00256 ); 00257 } 00258 $this->{$member}[$key] = (array)$value; 00259 } 00260 break; 00261 // Lists of strings 00262 case 'dependencies': 00263 case 'messages': 00264 case 'targets': 00265 // Normalise 00266 $option = array_values( array_unique( (array)$option ) ); 00267 sort( $option ); 00268 00269 $this->{$member} = $option; 00270 break; 00271 // Single strings 00272 case 'group': 00273 case 'position': 00274 case 'skipFunction': 00275 $this->{$member} = (string)$option; 00276 break; 00277 // Single booleans 00278 case 'debugRaw': 00279 case 'raw': 00280 $this->{$member} = (bool)$option; 00281 break; 00282 } 00283 } 00284 } 00285 00297 public static function extractBasePaths( 00298 $options = array(), 00299 $localBasePath = null, 00300 $remoteBasePath = null 00301 ) { 00302 global $IP, $wgResourceBasePath; 00303 00304 // The different ways these checks are done, and their ordering, look very silly, 00305 // but were preserved for backwards-compatibility just in case. Tread lightly. 00306 00307 $localBasePath = $localBasePath === null ? $IP : $localBasePath; 00308 if ( $remoteBasePath === null ) { 00309 $remoteBasePath = $wgResourceBasePath; 00310 } 00311 00312 if ( isset( $options['remoteExtPath'] ) ) { 00313 global $wgExtensionAssetsPath; 00314 $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; 00315 } 00316 00317 if ( isset( $options['remoteSkinPath'] ) ) { 00318 global $wgStylePath; 00319 $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath']; 00320 } 00321 00322 if ( array_key_exists( 'localBasePath', $options ) ) { 00323 $localBasePath = (string)$options['localBasePath']; 00324 } 00325 00326 if ( array_key_exists( 'remoteBasePath', $options ) ) { 00327 $remoteBasePath = (string)$options['remoteBasePath']; 00328 } 00329 00330 // Make sure the remote base path is a complete valid URL, 00331 // but possibly protocol-relative to avoid cache pollution 00332 $remoteBasePath = wfExpandUrl( $remoteBasePath, PROTO_RELATIVE ); 00333 00334 return array( $localBasePath, $remoteBasePath ); 00335 } 00336 00343 public function getScript( ResourceLoaderContext $context ) { 00344 $files = $this->getScriptFiles( $context ); 00345 return $this->readScriptFiles( $files ); 00346 } 00347 00352 public function getScriptURLsForDebug( ResourceLoaderContext $context ) { 00353 $urls = array(); 00354 foreach ( $this->getScriptFiles( $context ) as $file ) { 00355 $urls[] = $this->getRemotePath( $file ); 00356 } 00357 return $urls; 00358 } 00359 00363 public function supportsURLLoading() { 00364 return $this->debugRaw; 00365 } 00366 00372 public function getLoaderScript() { 00373 if ( count( $this->loaderScripts ) === 0 ) { 00374 return false; 00375 } 00376 return $this->readScriptFiles( $this->loaderScripts ); 00377 } 00378 00385 public function getStyles( ResourceLoaderContext $context ) { 00386 $styles = $this->readStyleFiles( 00387 $this->getStyleFiles( $context ), 00388 $this->getFlip( $context ), 00389 $context 00390 ); 00391 // Collect referenced files 00392 $this->localFileRefs = array_unique( $this->localFileRefs ); 00393 // If the list has been modified since last time we cached it, update the cache 00394 try { 00395 if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) ) { 00396 $dbw = wfGetDB( DB_MASTER ); 00397 $dbw->replace( 'module_deps', 00398 array( array( 'md_module', 'md_skin' ) ), array( 00399 'md_module' => $this->getName(), 00400 'md_skin' => $context->getSkin(), 00401 'md_deps' => FormatJson::encode( $this->localFileRefs ), 00402 ) 00403 ); 00404 } 00405 } catch ( Exception $e ) { 00406 wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" ); 00407 } 00408 return $styles; 00409 } 00410 00415 public function getStyleURLsForDebug( ResourceLoaderContext $context ) { 00416 if ( $this->hasGeneratedStyles ) { 00417 // Do the default behaviour of returning a url back to load.php 00418 // but with only=styles. 00419 return parent::getStyleURLsForDebug( $context ); 00420 } 00421 // Our module consists entirely of real css files, 00422 // in debug mode we can load those directly. 00423 $urls = array(); 00424 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { 00425 $urls[$mediaType] = array(); 00426 foreach ( $list as $file ) { 00427 $urls[$mediaType][] = $this->getRemotePath( $file ); 00428 } 00429 } 00430 return $urls; 00431 } 00432 00438 public function getMessages() { 00439 return $this->messages; 00440 } 00441 00447 public function getGroup() { 00448 return $this->group; 00449 } 00450 00454 public function getPosition() { 00455 return $this->position; 00456 } 00457 00463 public function getDependencies() { 00464 return $this->dependencies; 00465 } 00466 00472 public function getSkipFunction() { 00473 if ( !$this->skipFunction ) { 00474 return null; 00475 } 00476 00477 $localPath = $this->getLocalPath( $this->skipFunction ); 00478 if ( !file_exists( $localPath ) ) { 00479 throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" ); 00480 } 00481 $contents = file_get_contents( $localPath ); 00482 if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) { 00483 $contents = $this->validateScriptFile( $localPath, $contents ); 00484 } 00485 return $contents; 00486 } 00487 00491 public function isRaw() { 00492 return $this->raw; 00493 } 00494 00509 public function getModifiedTime( ResourceLoaderContext $context ) { 00510 if ( isset( $this->modifiedTime[$context->getHash()] ) ) { 00511 return $this->modifiedTime[$context->getHash()]; 00512 } 00513 wfProfileIn( __METHOD__ ); 00514 00515 $files = array(); 00516 00517 // Flatten style files into $files 00518 $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' ); 00519 foreach ( $styles as $styleFiles ) { 00520 $files = array_merge( $files, $styleFiles ); 00521 } 00522 00523 $skinFiles = self::collateFilePathListByOption( 00524 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 00525 'media', 00526 'all' 00527 ); 00528 foreach ( $skinFiles as $styleFiles ) { 00529 $files = array_merge( $files, $styleFiles ); 00530 } 00531 00532 // Final merge, this should result in a master list of dependent files 00533 $files = array_merge( 00534 $files, 00535 $this->scripts, 00536 $context->getDebug() ? $this->debugScripts : array(), 00537 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00538 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), 00539 $this->loaderScripts 00540 ); 00541 if ( $this->skipFunction ) { 00542 $files[] = $this->skipFunction; 00543 } 00544 $files = array_map( array( $this, 'getLocalPath' ), $files ); 00545 // File deps need to be treated separately because they're already prefixed 00546 $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); 00547 00548 // If a module is nothing but a list of dependencies, we need to avoid 00549 // giving max() an empty array 00550 if ( count( $files ) === 0 ) { 00551 $this->modifiedTime[$context->getHash()] = 1; 00552 wfProfileOut( __METHOD__ ); 00553 return $this->modifiedTime[$context->getHash()]; 00554 } 00555 00556 wfProfileIn( __METHOD__ . '-filemtime' ); 00557 $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); 00558 wfProfileOut( __METHOD__ . '-filemtime' ); 00559 00560 $this->modifiedTime[$context->getHash()] = max( 00561 $filesMtime, 00562 $this->getMsgBlobMtime( $context->getLanguage() ), 00563 $this->getDefinitionMtime( $context ) 00564 ); 00565 00566 wfProfileOut( __METHOD__ ); 00567 return $this->modifiedTime[$context->getHash()]; 00568 } 00569 00576 public function getDefinitionSummary( ResourceLoaderContext $context ) { 00577 $summary = array( 00578 'class' => get_class( $this ), 00579 ); 00580 foreach ( array( 00581 'scripts', 00582 'debugScripts', 00583 'loaderScripts', 00584 'styles', 00585 'languageScripts', 00586 'skinScripts', 00587 'skinStyles', 00588 'dependencies', 00589 'messages', 00590 'targets', 00591 'group', 00592 'position', 00593 'skipFunction', 00594 'localBasePath', 00595 'remoteBasePath', 00596 'debugRaw', 00597 'raw', 00598 ) as $member ) { 00599 $summary[$member] = $this->{$member}; 00600 }; 00601 return $summary; 00602 } 00603 00604 /* Protected Methods */ 00605 00610 protected function getLocalPath( $path ) { 00611 if ( $path instanceof ResourceLoaderFilePath ) { 00612 return $path->getLocalPath(); 00613 } 00614 00615 return "{$this->localBasePath}/$path"; 00616 } 00617 00622 protected function getRemotePath( $path ) { 00623 if ( $path instanceof ResourceLoaderFilePath ) { 00624 return $path->getRemotePath(); 00625 } 00626 00627 return "{$this->remoteBasePath}/$path"; 00628 } 00629 00637 public function getStyleSheetLang( $path ) { 00638 return preg_match( '/\.less$/i', $path ) ? 'less' : 'css'; 00639 } 00640 00650 protected static function collateFilePathListByOption( array $list, $option, $default ) { 00651 $collatedFiles = array(); 00652 foreach ( (array)$list as $key => $value ) { 00653 if ( is_int( $key ) ) { 00654 // File name as the value 00655 if ( !isset( $collatedFiles[$default] ) ) { 00656 $collatedFiles[$default] = array(); 00657 } 00658 $collatedFiles[$default][] = $value; 00659 } elseif ( is_array( $value ) ) { 00660 // File name as the key, options array as the value 00661 $optionValue = isset( $value[$option] ) ? $value[$option] : $default; 00662 if ( !isset( $collatedFiles[$optionValue] ) ) { 00663 $collatedFiles[$optionValue] = array(); 00664 } 00665 $collatedFiles[$optionValue][] = $key; 00666 } 00667 } 00668 return $collatedFiles; 00669 } 00670 00680 protected static function tryForKey( array $list, $key, $fallback = null ) { 00681 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { 00682 return $list[$key]; 00683 } elseif ( is_string( $fallback ) 00684 && isset( $list[$fallback] ) 00685 && is_array( $list[$fallback] ) 00686 ) { 00687 return $list[$fallback]; 00688 } 00689 return array(); 00690 } 00691 00698 protected function getScriptFiles( ResourceLoaderContext $context ) { 00699 $files = array_merge( 00700 $this->scripts, 00701 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00702 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) 00703 ); 00704 if ( $context->getDebug() ) { 00705 $files = array_merge( $files, $this->debugScripts ); 00706 } 00707 00708 return array_unique( $files, SORT_REGULAR ); 00709 } 00710 00717 public function getStyleFiles( ResourceLoaderContext $context ) { 00718 return array_merge_recursive( 00719 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00720 self::collateFilePathListByOption( 00721 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 00722 'media', 00723 'all' 00724 ) 00725 ); 00726 } 00727 00735 protected function getSkinStyleFiles( $skinName ) { 00736 return self::collateFilePathListByOption( 00737 self::tryForKey( $this->skinStyles, $skinName ), 00738 'media', 00739 'all' 00740 ); 00741 } 00742 00749 protected function getAllSkinStyleFiles() { 00750 $styleFiles = array(); 00751 $internalSkinNames = array_keys( Skin::getSkinNames() ); 00752 $internalSkinNames[] = 'default'; 00753 00754 foreach ( $internalSkinNames as $internalSkinName ) { 00755 $styleFiles = array_merge_recursive( 00756 $styleFiles, 00757 $this->getSkinStyleFiles( $internalSkinName ) 00758 ); 00759 } 00760 00761 return $styleFiles; 00762 } 00763 00769 public function getAllStyleFiles() { 00770 $collatedStyleFiles = array_merge_recursive( 00771 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00772 $this->getAllSkinStyleFiles() 00773 ); 00774 00775 $result = array(); 00776 00777 foreach ( $collatedStyleFiles as $media => $styleFiles ) { 00778 foreach ( $styleFiles as $styleFile ) { 00779 $result[] = $this->getLocalPath( $styleFile ); 00780 } 00781 } 00782 00783 return $result; 00784 } 00785 00793 protected function readScriptFiles( array $scripts ) { 00794 if ( empty( $scripts ) ) { 00795 return ''; 00796 } 00797 $js = ''; 00798 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) { 00799 $localPath = $this->getLocalPath( $fileName ); 00800 if ( !file_exists( $localPath ) ) { 00801 throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" ); 00802 } 00803 $contents = file_get_contents( $localPath ); 00804 if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) { 00805 // Static files don't really need to be checked as often; unlike 00806 // on-wiki module they shouldn't change unexpectedly without 00807 // admin interference. 00808 $contents = $this->validateScriptFile( $fileName, $contents ); 00809 } 00810 $js .= $contents . "\n"; 00811 } 00812 return $js; 00813 } 00814 00827 public function readStyleFiles( array $styles, $flip, $context = null ) { 00828 if ( empty( $styles ) ) { 00829 return array(); 00830 } 00831 foreach ( $styles as $media => $files ) { 00832 $uniqueFiles = array_unique( $files, SORT_REGULAR ); 00833 $styleFiles = array(); 00834 foreach ( $uniqueFiles as $file ) { 00835 $styleFiles[] = $this->readStyleFile( $file, $flip, $context ); 00836 } 00837 $styles[$media] = implode( "\n", $styleFiles ); 00838 } 00839 return $styles; 00840 } 00841 00854 protected function readStyleFile( $path, $flip, $context = null ) { 00855 $localPath = $this->getLocalPath( $path ); 00856 $remotePath = $this->getRemotePath( $path ); 00857 if ( !file_exists( $localPath ) ) { 00858 $msg = __METHOD__ . ": style file not found: \"$localPath\""; 00859 wfDebugLog( 'resourceloader', $msg ); 00860 throw new MWException( $msg ); 00861 } 00862 00863 if ( $this->getStyleSheetLang( $localPath ) === 'less' ) { 00864 $compiler = $this->getLessCompiler( $context ); 00865 $style = $this->compileLessFile( $localPath, $compiler ); 00866 $this->hasGeneratedStyles = true; 00867 } else { 00868 $style = file_get_contents( $localPath ); 00869 } 00870 00871 if ( $flip ) { 00872 $style = CSSJanus::transform( $style, true, false ); 00873 } 00874 $localDir = dirname( $localPath ); 00875 $remoteDir = dirname( $remotePath ); 00876 // Get and register local file references 00877 $this->localFileRefs = array_merge( 00878 $this->localFileRefs, 00879 CSSMin::getLocalFileReferences( $style, $localDir ) 00880 ); 00881 return CSSMin::remap( 00882 $style, $localDir, $remoteDir, true 00883 ); 00884 } 00885 00891 public function getFlip( $context ) { 00892 return $context->getDirection() === 'rtl'; 00893 } 00894 00900 public function getTargets() { 00901 return $this->targets; 00902 } 00903 00915 protected function compileLessFile( $fileName, $compiler = null ) { 00916 if ( !$compiler ) { 00917 $compiler = $this->getLessCompiler(); 00918 } 00919 $result = $compiler->compileFile( $fileName ); 00920 $this->localFileRefs += array_keys( $compiler->allParsedFiles() ); 00921 return $result; 00922 } 00923 00934 protected function getLessCompiler( ResourceLoaderContext $context = null ) { 00935 return ResourceLoader::getLessCompiler( $this->getConfig() ); 00936 } 00937 }