MediaWiki  REL1_24
ResourceLoaderFileModule.php
Go to the documentation of this file.
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 }