MediaWiki
REL1_19
|
00001 <?php 00026 class ResourceLoaderFileModule extends ResourceLoaderModule { 00027 00028 /* Protected Members */ 00029 00031 protected $localBasePath = ''; 00033 protected $remoteBasePath = ''; 00041 protected $scripts = array(); 00049 protected $languageScripts = array(); 00057 protected $skinScripts = array(); 00065 protected $debugScripts = array(); 00073 protected $loaderScripts = array(); 00081 protected $styles = array(); 00089 protected $skinStyles = array(); 00097 protected $dependencies = array(); 00105 protected $messages = array(); 00107 protected $group; 00109 protected $position = 'bottom'; 00111 protected $debugRaw = true; 00119 protected $modifiedTime = array(); 00127 protected $localFileRefs = array(); 00128 00129 /* Methods */ 00130 00182 public function __construct( $options = array(), $localBasePath = null, 00183 $remoteBasePath = null ) 00184 { 00185 global $IP, $wgScriptPath, $wgResourceBasePath; 00186 $this->localBasePath = $localBasePath === null ? $IP : $localBasePath; 00187 if ( $remoteBasePath !== null ) { 00188 $this->remoteBasePath = $remoteBasePath; 00189 } else { 00190 $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath; 00191 } 00192 00193 if ( isset( $options['remoteExtPath'] ) ) { 00194 global $wgExtensionAssetsPath; 00195 $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; 00196 } 00197 00198 foreach ( $options as $member => $option ) { 00199 switch ( $member ) { 00200 // Lists of file paths 00201 case 'scripts': 00202 case 'debugScripts': 00203 case 'loaderScripts': 00204 case 'styles': 00205 $this->{$member} = (array) $option; 00206 break; 00207 // Collated lists of file paths 00208 case 'languageScripts': 00209 case 'skinScripts': 00210 case 'skinStyles': 00211 if ( !is_array( $option ) ) { 00212 throw new MWException( 00213 "Invalid collated file path list error. " . 00214 "'$option' given, array expected." 00215 ); 00216 } 00217 foreach ( $option as $key => $value ) { 00218 if ( !is_string( $key ) ) { 00219 throw new MWException( 00220 "Invalid collated file path list key error. " . 00221 "'$key' given, string expected." 00222 ); 00223 } 00224 $this->{$member}[$key] = (array) $value; 00225 } 00226 break; 00227 // Lists of strings 00228 case 'dependencies': 00229 case 'messages': 00230 $this->{$member} = (array) $option; 00231 break; 00232 // Single strings 00233 case 'group': 00234 case 'position': 00235 case 'localBasePath': 00236 case 'remoteBasePath': 00237 $this->{$member} = (string) $option; 00238 break; 00239 // Single booleans 00240 case 'debugRaw': 00241 $this->{$member} = (bool) $option; 00242 break; 00243 } 00244 } 00245 // Make sure the remote base path is a complete valid URL, 00246 // but possibly protocol-relative to avoid cache pollution 00247 $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE ); 00248 } 00249 00256 public function getScript( ResourceLoaderContext $context ) { 00257 $files = $this->getScriptFiles( $context ); 00258 return $this->readScriptFiles( $files ); 00259 } 00260 00265 public function getScriptURLsForDebug( ResourceLoaderContext $context ) { 00266 $urls = array(); 00267 foreach ( $this->getScriptFiles( $context ) as $file ) { 00268 $urls[] = $this->getRemotePath( $file ); 00269 } 00270 return $urls; 00271 } 00272 00276 public function supportsURLLoading() { 00277 return $this->debugRaw; 00278 } 00279 00285 public function getLoaderScript() { 00286 if ( count( $this->loaderScripts ) == 0 ) { 00287 return false; 00288 } 00289 return $this->readScriptFiles( $this->loaderScripts ); 00290 } 00291 00298 public function getStyles( ResourceLoaderContext $context ) { 00299 $styles = $this->readStyleFiles( 00300 $this->getStyleFiles( $context ), 00301 $this->getFlip( $context ) 00302 ); 00303 // Collect referenced files 00304 $this->localFileRefs = array_unique( $this->localFileRefs ); 00305 // If the list has been modified since last time we cached it, update the cache 00306 if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) && !wfReadOnly() ) { 00307 $dbw = wfGetDB( DB_MASTER ); 00308 $dbw->replace( 'module_deps', 00309 array( array( 'md_module', 'md_skin' ) ), array( 00310 'md_module' => $this->getName(), 00311 'md_skin' => $context->getSkin(), 00312 'md_deps' => FormatJson::encode( $this->localFileRefs ), 00313 ) 00314 ); 00315 } 00316 return $styles; 00317 } 00318 00323 public function getStyleURLsForDebug( ResourceLoaderContext $context ) { 00324 $urls = array(); 00325 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { 00326 $urls[$mediaType] = array(); 00327 foreach ( $list as $file ) { 00328 $urls[$mediaType][] = $this->getRemotePath( $file ); 00329 } 00330 } 00331 return $urls; 00332 } 00333 00339 public function getMessages() { 00340 return $this->messages; 00341 } 00342 00348 public function getGroup() { 00349 return $this->group; 00350 } 00351 00355 public function getPosition() { 00356 return $this->position; 00357 } 00358 00364 public function getDependencies() { 00365 return $this->dependencies; 00366 } 00367 00382 public function getModifiedTime( ResourceLoaderContext $context ) { 00383 if ( isset( $this->modifiedTime[$context->getHash()] ) ) { 00384 return $this->modifiedTime[$context->getHash()]; 00385 } 00386 wfProfileIn( __METHOD__ ); 00387 00388 $files = array(); 00389 00390 // Flatten style files into $files 00391 $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' ); 00392 foreach ( $styles as $styleFiles ) { 00393 $files = array_merge( $files, $styleFiles ); 00394 } 00395 $skinFiles = self::tryForKey( 00396 self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ), 00397 $context->getSkin(), 00398 'default' 00399 ); 00400 foreach ( $skinFiles as $styleFiles ) { 00401 $files = array_merge( $files, $styleFiles ); 00402 } 00403 00404 // Final merge, this should result in a master list of dependent files 00405 $files = array_merge( 00406 $files, 00407 $this->scripts, 00408 $context->getDebug() ? $this->debugScripts : array(), 00409 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00410 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), 00411 $this->loaderScripts 00412 ); 00413 $files = array_map( array( $this, 'getLocalPath' ), $files ); 00414 // File deps need to be treated separately because they're already prefixed 00415 $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); 00416 00417 // If a module is nothing but a list of dependencies, we need to avoid 00418 // giving max() an empty array 00419 if ( count( $files ) === 0 ) { 00420 wfProfileOut( __METHOD__ ); 00421 return $this->modifiedTime[$context->getHash()] = 1; 00422 } 00423 00424 wfProfileIn( __METHOD__.'-filemtime' ); 00425 $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); 00426 wfProfileOut( __METHOD__.'-filemtime' ); 00427 $this->modifiedTime[$context->getHash()] = max( 00428 $filesMtime, 00429 $this->getMsgBlobMtime( $context->getLanguage() ) ); 00430 00431 wfProfileOut( __METHOD__ ); 00432 return $this->modifiedTime[$context->getHash()]; 00433 } 00434 00435 /* Protected Methods */ 00436 00441 protected function getLocalPath( $path ) { 00442 return "{$this->localBasePath}/$path"; 00443 } 00444 00449 protected function getRemotePath( $path ) { 00450 return "{$this->remoteBasePath}/$path"; 00451 } 00452 00462 protected static function collateFilePathListByOption( array $list, $option, $default ) { 00463 $collatedFiles = array(); 00464 foreach ( (array) $list as $key => $value ) { 00465 if ( is_int( $key ) ) { 00466 // File name as the value 00467 if ( !isset( $collatedFiles[$default] ) ) { 00468 $collatedFiles[$default] = array(); 00469 } 00470 $collatedFiles[$default][] = $value; 00471 } elseif ( is_array( $value ) ) { 00472 // File name as the key, options array as the value 00473 $optionValue = isset( $value[$option] ) ? $value[$option] : $default; 00474 if ( !isset( $collatedFiles[$optionValue] ) ) { 00475 $collatedFiles[$optionValue] = array(); 00476 } 00477 $collatedFiles[$optionValue][] = $key; 00478 } 00479 } 00480 return $collatedFiles; 00481 } 00482 00492 protected static function tryForKey( array $list, $key, $fallback = null ) { 00493 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { 00494 return $list[$key]; 00495 } elseif ( is_string( $fallback ) 00496 && isset( $list[$fallback] ) 00497 && is_array( $list[$fallback] ) ) 00498 { 00499 return $list[$fallback]; 00500 } 00501 return array(); 00502 } 00503 00510 protected function getScriptFiles( ResourceLoaderContext $context ) { 00511 $files = array_merge( 00512 $this->scripts, 00513 self::tryForKey( $this->languageScripts, $context->getLanguage() ), 00514 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) 00515 ); 00516 if ( $context->getDebug() ) { 00517 $files = array_merge( $files, $this->debugScripts ); 00518 } 00519 return $files; 00520 } 00521 00528 protected function getStyleFiles( ResourceLoaderContext $context ) { 00529 return array_merge_recursive( 00530 self::collateFilePathListByOption( $this->styles, 'media', 'all' ), 00531 self::collateFilePathListByOption( 00532 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' 00533 ) 00534 ); 00535 } 00536 00543 protected function readScriptFiles( array $scripts ) { 00544 global $wgResourceLoaderValidateStaticJS; 00545 if ( empty( $scripts ) ) { 00546 return ''; 00547 } 00548 $js = ''; 00549 foreach ( array_unique( $scripts ) as $fileName ) { 00550 $localPath = $this->getLocalPath( $fileName ); 00551 if ( !file_exists( $localPath ) ) { 00552 throw new MWException( __METHOD__.": script file not found: \"$localPath\"" ); 00553 } 00554 $contents = file_get_contents( $localPath ); 00555 if ( $wgResourceLoaderValidateStaticJS ) { 00556 // Static files don't really need to be checked as often; unlike 00557 // on-wiki module they shouldn't change unexpectedly without 00558 // admin interference. 00559 $contents = $this->validateScriptFile( $fileName, $contents ); 00560 } 00561 $js .= $contents . "\n"; 00562 } 00563 return $js; 00564 } 00565 00577 protected function readStyleFiles( array $styles, $flip ) { 00578 if ( empty( $styles ) ) { 00579 return array(); 00580 } 00581 foreach ( $styles as $media => $files ) { 00582 $uniqueFiles = array_unique( $files ); 00583 $styles[$media] = implode( 00584 "\n", 00585 array_map( 00586 array( $this, 'readStyleFile' ), 00587 $uniqueFiles, 00588 array_fill( 0, count( $uniqueFiles ), $flip ) 00589 ) 00590 ); 00591 } 00592 return $styles; 00593 } 00594 00606 protected function readStyleFile( $path, $flip ) { 00607 $localPath = $this->getLocalPath( $path ); 00608 if ( !file_exists( $localPath ) ) { 00609 throw new MWException( __METHOD__.": style file not found: \"$localPath\"" ); 00610 } 00611 $style = file_get_contents( $localPath ); 00612 if ( $flip ) { 00613 $style = CSSJanus::transform( $style, true, false ); 00614 } 00615 $dirname = dirname( $path ); 00616 if ( $dirname == '.' ) { 00617 // If $path doesn't have a directory component, don't prepend a dot 00618 $dirname = ''; 00619 } 00620 $dir = $this->getLocalPath( $dirname ); 00621 $remoteDir = $this->getRemotePath( $dirname ); 00622 // Get and register local file references 00623 $this->localFileRefs = array_merge( 00624 $this->localFileRefs, 00625 CSSMin::getLocalFileReferences( $style, $dir ) ); 00626 return CSSMin::remap( 00627 $style, $dir, $remoteDir, true 00628 ); 00629 } 00630 00637 protected static function safeFilemtime( $filename ) { 00638 if ( file_exists( $filename ) ) { 00639 return filemtime( $filename ); 00640 } else { 00641 // We only ever map this function on an array if we're gonna call max() after, 00642 // so return our standard minimum timestamps here. This is 1, not 0, because 00643 // wfTimestamp(0) == NOW 00644 return 1; 00645 } 00646 } 00647 00653 public function getFlip( $context ) { 00654 return $context->getDirection() === 'rtl'; 00655 } 00656 }