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