* added in language mapping for commons upload form language hack (was supposed to...
[lhc/web/wiklou.git] / js2 / mwEmbed / jsScriptLoader.php
1 <?php
2 /**
3 * This core jsScriptLoader class provides the script loader functionality
4 * @file
5 */
6
7
8 //Setup the script local script cache directory (has to be hard coded rather than config based for fast non-mediawiki config hits
9 $wgScriptCacheDirectory = realpath( dirname( __FILE__ ) ) . '/php/script-cache';
10
11 // Check if we are being invoked in a MediaWiki context or stand alone usage:
12 if ( !defined( 'MEDIAWIKI' ) && !defined( 'MW_CACHE_SCRIPT_CHECK' ) ){
13 // Load noMediaWiki helper for quick cache result
14 $myScriptLoader = new jsScriptLoader();
15 if( $myScriptLoader->outputFromCache() )
16 exit();
17 //Else load up all the config and do normal stand alone ScriptLoader process:
18 require_once( realpath( dirname( __FILE__ ) ) . '/php/noMediaWikiConfig.php' );
19 $myScriptLoader->doScriptLoader();
20 }
21
22 class jsScriptLoader {
23 var $jsFileList = array();
24 var $langCode = '';
25 var $jsout = '';
26 var $rKey = ''; // the request key
27 var $error_msg = '';
28 var $debug = false;
29 var $jsvarurl = false; // whether we should include generated JS (special class '-')
30 var $doProcReqFlag = true;
31
32 function outputFromCache(){
33 // Process the request
34 $this->rKey = $this->preProcRequestVars();
35 // Setup file cache object
36 $this->sFileCache = new simpleFileCache( $this->rKey );
37 if ( $this->sFileCache->isFileCached() ) {
38 // Just output headers so we can use PHP's @readfile::
39 $this->outputJsHeaders();
40 $this->sFileCache->outputFromFileCache();
41 return true;
42 }
43 return false;
44 }
45
46 function doScriptLoader() {
47 global $wgJSAutoloadClasses, $wgJSAutoloadLocalClasses, $IP,
48 $wgEnableScriptMinify, $wgUseFileCache, $wgExtensionMessagesFiles;
49
50 //load the ExtensionMessagesFiles
51 $wgExtensionMessagesFiles['mwEmbed'] = realpath( dirname( __FILE__ ) ) . '/php/languages/mwEmbed.i18n.php';
52
53 //reset the rKey:
54 $this->rKey = '';
55 //do the post proc request with configuration vars:
56 $this->postProcRequestVars();
57 //update the filename (if gzip is on)
58 $this->sFileCache->getCacheFileName();
59
60 // Setup script loader header info
61 // @@todo we might want to put these into the mw var per class request set
62 // and or include a callback to avoid pulling in old browsers that don't support
63 // the onLoad attribute for script elements.
64 $this->jsout .= 'var mwSlScript = "' . $_SERVER['SCRIPT_NAME'] . '";' . "\n";
65 $this->jsout .= 'var mwSlGenISODate = "' . date( 'c' ) . '";' . "\n";
66 $this->jsout .= 'var mwSlURID = "' . htmlspecialchars( $this->urid ) . '";' . "\n";
67 $this->jsout .= 'var mwLang = "' . htmlspecialchars( $this->langCode ) . '";' . "\n";
68 // Build the output
69
70 // Swap in the appropriate language per js_file
71 foreach ( $this->jsFileList as $classKey => $file_name ) {
72 //get the script content
73 $jstxt = $this->getScriptText($classKey, $file_name);
74 if( $jstxt ){
75 $this->jsout .= $this->doProcessJs( $jstxt );
76 }
77 }
78 // Check if we should minify the whole thing:
79 if ( !$this->debug ) {
80 // do the minification and output
81 $this->jsout = JSMin::minify( $this->jsout );
82 }
83 // Save to the file cache
84 if ( $wgUseFileCache && !$this->debug ) {
85 $status = $this->sFileCache->saveToFileCache( $this->jsout );
86 if ( $status !== true )
87 $this->error_msg .= $status;
88 }
89 // Check for an error msg
90 if ( $this->error_msg != '' ) {
91 //just set the content type (don't send cache header)
92 header( 'Content-Type: text/javascript' );
93 echo 'alert(\'Error With ScriptLoader.php ::' . str_replace( "\n", '\'+"\n"+' . "\n'", $this->error_msg ) . '\');';
94 echo trim( $this->jsout );
95 } else {
96 // All good, let's output "cache" headers
97 $this->outputJsWithHeaders();
98 }
99 }
100 function getScriptText($classKey, $file_name=''){
101 $jsout = '';
102 // Special case: title classes
103 if ( substr( $classKey, 0, 3 ) == 'WT:' ) {
104 global $wgUser;
105 // Get just the title part
106 $title_block = substr( $classKey, 3 );
107 if ( $title_block[0] == '-' && strpos( $title_block, '|' ) !== false ) {
108 // Special case of "-" title with skin
109 $parts = explode( '|', $title_block );
110 $title = array_shift( $parts );
111 foreach ( $parts as $tparam ) {
112 list( $key, $val ) = explode( '=', $tparam );
113 if ( $key == 'useskin' ) {
114 $skin = $val;
115 }
116 }
117 $sk = $wgUser->getSkin();
118 // Make sure the skin name is valid
119 $skinNames = Skin::getSkinNames();
120 $skinNames = array_keys( $skinNames );
121 if ( in_array( strtolower( $skin ), $skinNames ) ) {
122 // If in debug mode, add a comment with wiki title and rev:
123 if ( $this->debug )
124 $jsout .= "\n/**\n* GenerateUserJs: \n*/\n";
125 return $jsout . $sk->generateUserJs( $skin ) . "\n";
126 }
127 } else {
128 // Make sure the wiki title ends with .js
129 if ( substr( $title_block, - 3 ) != '.js' ) {
130 $this->error_msg .= 'WikiTitle includes should end with .js';
131 return false;
132 }
133 // It's a wiki title, append the output of the wikitext:
134 $t = Title::newFromText( $title_block );
135 $a = new Article( $t );
136 // Only get the content if the page is not empty:
137 if ( $a->getID() !== 0 ) {
138 // If in debug mode, add a comment with wiki title and rev:
139 if ( $this->debug )
140 $jsout .= "\n/**\n* WikiJSPage: " . htmlspecialchars( $title_block ) . " rev: " . $a->getID() . " \n*/\n";
141
142 return $jsout . $a->getContent() . "\n";
143 }
144 }
145 }else{
146 // Dealing with files
147
148 // Check that the filename ends with .js and does not include ../ traversing
149 if ( substr( $file_name, - 3 ) != '.js' ) {
150 $this->error_msg .= "\nError file name must end with .js: " . htmlspecialchars( $file_name ) . " \n ";
151 return false;
152 }
153 if ( strpos( $file_name, '../' ) !== false ) {
154 $this->error_msg .= "\nError file name must not traverse paths: " . htmlspecialchars( $file_name ) . " \n ";
155 return false;
156 }
157
158 if ( trim( $file_name ) != '' ) {
159 if ( $this->debug )
160 $jsout .= "\n/**\n* File: " . htmlspecialchars( $file_name ) . "\n*/\n";
161
162 $jsFileStr = $this->doGetJsFile( $file_name ) . "\n";
163 if( $jsFileStr ){
164 return $jsout . $jsFileStr;
165 }else{
166 $this->error_msg .= "\nError could not read file: ". htmlspecialchars( $file_name ) ."\n";
167 return false;
168 }
169 }
170 }
171 //if we did not return some js
172 $this->error_msg .= "\nUnknown error\n";
173 return false;
174 }
175 function outputJsHeaders() {
176 // Output JS MIME type:
177 header( 'Content-Type: text/javascript' );
178 header( 'Pragma: public' );
179 // Cache for 1 day ( we should always change the request URL
180 // based on the SVN or article version.
181 $one_day = 60 * 60 * 24;
182 header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $one_day ) . " GM" );
183 }
184
185 function outputJsWithHeaders() {
186 global $wgUseGzip;
187 $this->outputJsHeaders();
188 if ( $wgUseGzip ) {
189 if ( $this->clientAcceptsGzip() ) {
190 header( 'Content-Encoding: gzip' );
191 echo gzencode( $this->jsout );
192 } else {
193 echo $this->jsout;
194 }
195 } else {
196 echo $this->jsout;
197 }
198 }
199
200 function clientAcceptsGzip() {
201 $m = array();
202 if( preg_match(
203 '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/',
204 $_SERVER['HTTP_ACCEPT_ENCODING'],
205 $m ) ) {
206 if( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) )
207 return false;
208 //no gzip support found
209 return true;
210 }
211 return false;
212 }
213 /*
214 * postProcRequestVars uses globals, configuration and mediaWiki to test wiki-titles and files exist etc.
215 */
216 function postProcRequestVars(){
217 global $wgContLanguageCode, $wgEnableScriptMinify, $wgJSAutoloadClasses,
218 $wgJSAutoloadLocalClasses, $wgStyleVersion;
219
220 // Set debug flag
221 if ( ( isset( $_GET['debug'] ) && $_GET['debug'] == 'true' ) || ( isset( $wgEnableScriptDebug ) && $wgEnableScriptDebug == true ) ) {
222 $this->debug = true;
223 }
224
225 // Set the urid. Be sure to escape it as it goes into our JS output.
226 if ( isset( $_GET['urid'] ) && $_GET['urid'] != '' ) {
227 $this->urid = htmlspecialchars( $_GET['urid'] );
228 } else {
229 // Just give it the current style sheet ID:
230 // @@todo read the svn version number
231 $this->urid = $wgStyleVersion;
232 }
233
234 //get the language code (if not provided use the "default" language
235 if ( isset( $_GET['uselang'] ) && $_GET['uselang'] != '' ) {
236 //make sure its a valid lang code:
237 $this->langCode = preg_replace( "/[^A-Za-z]/", '', $_GET['uselang']);
238 }else{
239 //set english as default
240 $this->langCode = 'en';
241 }
242 $this->langCode = self::checkForCommonsLanguageFormHack( $this->langCode );
243
244 $reqClassList = false;
245 if ( isset( $_GET['class'] ) && $_GET['class'] != '' ) {
246 $reqClassList = explode( ',', $_GET['class'] );
247 }
248
249 // Check for the requested classes
250 if ( $reqClassList ) {
251 // Clean the class list and populate jsFileList
252 foreach ( $reqClassList as $reqClass ) {
253 if ( trim( $reqClass ) != '' ) {
254 if ( substr( $reqClass, 0, 3 ) == 'WT:' ) {
255 $doAddWT = false;
256 // Check for special case '-' class for user-generated JS
257 if( substr( $reqClass, 3, 1) == '-'){
258 $doAddWT = true;
259 }else{
260 if( strtolower( substr( $reqClass, -3) ) == '.js'){
261 //make sure its a valid wikipage before doing processing
262 $t = Title::newFromDBkey( substr( $reqClass, 3) );
263 if( $t->exists()
264 && ( $t->getNamespace() == NS_MEDIAWIKI
265 || $t->getNamespace() == NS_USER ) ){
266 $doAddWT = true;
267 }
268 }
269 }
270 if( $doAddWT ){
271 $this->jsFileList[$reqClass] = true;
272 $this->rKey .= $reqClass;
273 $this->jsvarurl = true;
274 }
275 continue;
276 }
277
278 $reqClass = preg_replace( "/[^A-Za-z0-9_\-\.]/", '', $reqClass );
279
280 $jsFilePath = self::getJsPathFromClass( $reqClass );
281 if(!$jsFilePath){
282 $this->error_msg .= 'Requested class: ' . htmlspecialchars( $reqClass ) . ' not found' . "\n";
283 }else{
284 $this->jsFileList[ $reqClass ] = $jsFilePath;
285 $this->rKey .= $reqClass;
286 }
287 }
288 }
289 }
290
291
292 // Add the language code to the rKey:
293 $this->rKey .= '_' . $wgContLanguageCode;
294
295 // Add the unique rid
296 $this->rKey .= $this->urid;
297
298 // Add a minify flag
299 if ( $wgEnableScriptMinify ) {
300 $this->rKey .= '_min';
301 }
302 }
303 /**
304 * Pre-process request variables ~without configuration~ or much utility function~
305 * This is to quickly get a rKey that we can check against the cache
306 */
307 function preProcRequestVars() {
308 $rKey = '';
309 // Check for debug (won't use the cache)
310 if ( ( isset( $_GET['debug'] ) && $_GET['debug'] == 'true' ) ) {
311 //we are going to have to run postProcRequest
312 return false;
313 }
314
315 // Check for the urid. Be sure to escape it as it goes into our JS output.
316 if ( isset( $_GET['urid'] ) && $_GET['urid'] != '' ) {
317 $urid = htmlspecialchars( $_GET['urid'] );
318 }else{
319 die( 'missing urid param');
320 }
321
322 //get the language code (if not provided use the "default" language
323 if ( isset( $_GET['uselang'] ) && $_GET['uselang'] != '' ) {
324 //make sure its a valid lang code:
325 $langCode = preg_replace( "/[^A-Za-z]/", '', $_GET['uselang']);
326 }else{
327 //set english as default
328 $langCode = 'en';
329 }
330
331 $langCode = self::checkForCommonsLanguageFormHack( $langCode );
332
333
334 $reqClassList = false;
335 if ( isset( $_GET['class'] ) && $_GET['class'] != '' ) {
336 $reqClassList = explode( ',', $_GET['class'] );
337 }
338
339 // Check for the requested classes
340 if ( count( $reqClassList ) > 0 ) {
341 // Clean the class list and populate jsFileList
342 foreach ( $reqClassList as $reqClass ) {
343 //do some simple checks:
344 if ( trim( $reqClass ) != '' ){
345 if( substr( $reqClass, 0, 3 ) == 'WT:' && strtolower( substr( $reqClass, -3) ) == '.js' ){
346 //wiki page requests (must end with .js):
347 $rKey .= $reqClass;
348 }else if( substr( $reqClass, 0, 3 ) != 'WT:' ){
349 //normal class requests:
350 $reqClass = preg_replace( "/[^A-Za-z0-9_\-\.]/", '', $reqClass );
351 $rKey .= $reqClass;
352 }else{
353 //not a valid class don't add it
354 }
355 }
356 }
357 }
358 // Add the language code to the rKey:
359 $rKey .= '_' . $langCode;
360
361 // Add the unique rid
362 $rKey .= $urid;
363
364 return $rKey;
365 }
366 public static function checkForCommonsLanguageFormHack( $langKey){
367 $formNames = array( 'ownwork', 'fromflickr', 'fromwikimedia', 'fromgov');
368 foreach($formNames as $formName){
369 // Directly reference a form Name then its "english"
370 if( $formName == $langKey )
371 return 'en';
372 // If the langKey includes a form name (ie esownwork)
373 // then strip the form name use that as the language key
374 if( strpos($langKey, $formName)!==false){
375 return str_replace($formName, '', $langKey);
376 }
377 }
378 //else just return the key unchanged:
379 return $langKey;
380 }
381 public static function getJsPathFromClass( $reqClass ){
382 global $wgJSAutoloadLocalClasses, $wgJSAutoloadClasses;
383 if ( isset( $wgJSAutoloadLocalClasses[$reqClass] ) ) {
384 return $wgJSAutoloadLocalClasses[$reqClass];
385 } else if ( isset( $wgJSAutoloadClasses[$reqClass] ) ) {
386 return $wgJSAutoloadClasses[$reqClass];
387 } else {
388 return false;
389 }
390 }
391 function doGetJsFile( $file_path ) {
392 global $IP;
393 // Load the file
394 $str = @file_get_contents( "{$IP}/{$file_path}" );
395
396 if ( $str === false ) {
397 // @@todo check PHP error level. Don't want to expose paths if errors are hidden.
398 $this->error_msg .= 'Requested File: ' . htmlspecialchars( $file_path ) . ' could not be read' . "\n";
399 return false;
400 }
401 return $str;
402 }
403 function doProcessJs( $str ){
404 global $wgEnableScriptLocalization;
405 // Strip out js_log debug lines (if not in debug mode)
406 if( !$this->debug )
407 $str = preg_replace('/\n\s*js_log\(([^\)]*\))*\s*[\;\n]/U', "\n", $str);
408
409 // Do language swap by index:
410 if ( $wgEnableScriptLocalization ){
411 $inx = self::getLoadGmIndex( $str );
412 if($inx){
413 $translated = $this->languageMsgReplace( substr($str, $inx['s'], ($inx['e']-$inx['s']) ));
414 //return the final string (without double {})
415 return substr($str, 0, $inx['s']-1) . $translated . substr($str, $inx['e']+1);
416 }
417 }
418 //return the js str unmodified if we did not transform with the localisation.
419 return $str;
420 }
421 static public function getLoadGmIndex( $str ){
422 $returnIndex = array();
423 preg_match('/loadGM\s*\(\s*\{/', $str, $matches, PREG_OFFSET_CAPTURE );
424 if( count($matches) == 0){
425 return false;
426 }
427 if( count( $matches ) > 0 ){
428 //offset + match str length gives startIndex:
429 $returnIndex['s'] = strlen( $matches[0][0] ) + $matches[0][1];
430 $foundMatch = true;
431 }
432 $ignorenext = false;
433 $inquote = false;
434 //look for closing } not inside quotes::
435 for ( $i = $returnIndex['s']; $i < strlen( $str ); $i++ ) {
436 $char = $str[$i];
437 if ( $ignorenext ) {
438 $ignorenext = false;
439 } else {
440 //search for a close } that is not in quotes or escaped
441 switch( $char ) {
442 case '"':
443 $inquote = !$inquote;
444 break;
445 case '}':
446 if( ! $inquote){
447 $returnIndex['e'] =$i;
448 return $returnIndex;
449 }
450 break;
451 case '\\':
452 if ( $inquote ) $ignorenext = true;
453 break;
454 }
455 }
456 }
457 }
458
459 function getInlineLoadGMFromClass( $class ){
460 $jsmsg = $this->getMsgKeysFromClass( $class );
461 if( $jsmsg ){
462 self::getMsgKeys ( $jsmsg );
463 return 'loadGM(' . FormatJson::encode( $jsmsg ) . ');';
464 }else{
465 //if could not parse return empty string:
466 return '';
467 }
468 }
469 function getMsgKeysFromClass( $class ){
470 $file_path = self::getJsPathFromClass( $class );
471 $str = $this->getScriptText($class, $file_path);
472
473 $inx = self::getLoadGmIndex( $str );
474 if(!$inx)
475 return '';
476 return FormatJson::decode( '{' . substr($str, $inx['s'], ($inx['e']-$inx['s'])) . '}', true);
477 }
478
479 static public function getMsgKeys(& $jmsg, $langCode = false){
480 global $wgContLanguageCode;
481 //check the langCode
482 if(!$langCode)
483 $langCode = $wgContLanguageCode;
484
485 // Get the msg keys for the a json array
486 foreach ( $jmsg as $msgKey => $default_en_value ) {
487 $jmsg[$msgKey] = wfMsgGetKey( $msgKey, true, $langCode, false );
488 }
489 }
490 function languageMsgReplace( $json_str ) {
491 $jmsg = FormatJson::decode( '{' . $json_str . '}', true );
492 // Do the language lookup
493 if ( $jmsg ) {
494 //see if any msgKey has the PLURAL template tag
495 //package in PLURAL mapping
496 self::getMsgKeys($jmsg, $this->langCode);
497
498 // Return the updated loadGM JSON with updated msgs:
499 return FormatJson::encode( $jmsg );
500 } else {
501 // Could not parse JSON return error: (maybe a alert?)
502 //we just make a note in the code, visitors will get the fallback language,
503 //developers will read the js source when its not behaving as expected.
504 return "\n/*
505 * Could not parse JSON language messages in this file,
506 * Please check that loadGM call contains valid JSON (not javascript)
507 */\n\n" . $json_str; //include the original fallback loadGM
508
509 }
510 }
511 }
512
513 // A simple version of HTMLFileCache (@@todo abstract shared pieces)
514 class simpleFileCache {
515 var $mFileCache;
516 var $filename = null;
517 var $rKey = null;
518
519 public function __construct( &$rKey ) {
520 $this->rKey = $rKey;
521 $this->getCacheFileName();
522 }
523
524 public function getCacheFileName() {
525 global $wgUseGzip, $wgScriptCacheDirectory;
526
527 $hash = md5( $this->rKey );
528 # Avoid extension confusion
529 $key = str_replace( '.', '%2E', urlencode( $this->rKey ) );
530
531 $hash1 = substr( $hash, 0, 1 );
532 $hash2 = substr( $hash, 0, 2 );
533 $this->filename = "{$wgScriptCacheDirectory}/{$hash1}/{$hash2}/{$this->rKey}.js";
534
535 // Check for defined files::
536 if( is_file( $this->filename ) )
537 return $this->filename;
538
539 if( is_file( $this->filename .'.gz') ){
540 $this->filename.='.gz';
541 return $this->filename;
542 }
543 //check the update the name based on the $wgUseGzip config var
544 if ( isset($wgUseGzip) && $wgUseGzip )
545 $this->filename.='.gz';
546 }
547
548 public function isFileCached() {
549 return file_exists( $this->filename );
550 }
551
552 public function outputFromFileCache() {
553 if ( $this->clientAcceptsGzip() && substr( $this->filename, - 3 ) == '.gz' ) {
554 header( 'Content-Encoding: gzip' );
555 readfile( $this->filename );
556 return true;
557 }
558 // Output without gzip:
559 if ( substr( $this->filename, - 3 ) == '.gz' ) {
560 readgzfile( $this->filename );
561 } else {
562 readfile( $this->filename );
563 }
564 return true;
565 }
566 public function clientAcceptsGzip(){
567 $m = array();
568 if ( preg_match(
569 '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/',
570 $_SERVER['HTTP_ACCEPT_ENCODING'],
571 $m ) ) {
572 if ( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) )
573 return false;
574
575 return true;
576 }
577 return false;
578 }
579 public function saveToFileCache( &$text ) {
580 global $wgUseFileCache, $wgUseGzip;
581 if ( !$wgUseFileCache ) {
582 return 'Error: Called saveToFileCache with $wgUseFileCache off';
583 }
584 if ( strcmp( $text, '' ) == 0 )
585 return 'saveToFileCache: empty output file';
586
587 if ( $wgUseGzip ) {
588 $outputText = gzencode( trim( $text ) );
589 } else {
590 $outputText = trim( $text );
591 }
592
593 // Check the directories. If we could not create them, error out.
594 $status = $this->checkCacheDirs();
595
596 if ( $status !== true )
597 return $status;
598 $f = fopen( $this->filename, 'w' );
599 if ( $f ) {
600 fwrite( $f, $outputText );
601 fclose( $f );
602 } else {
603 return 'Could not open file for writing. Check your cache directory permissions?';
604 }
605 return true;
606 }
607
608 protected function checkCacheDirs() {
609 $mydir2 = substr( $this->filename, 0, strrpos( $this->filename, '/' ) ); # subdirectory level 2
610 $mydir1 = substr( $mydir2, 0, strrpos( $mydir2, '/' ) ); # subdirectory level 1
611
612 if ( wfMkdirParents( $mydir1 ) === false || wfMkdirParents( $mydir2 ) === false ) {
613 return 'Could not create cache directory. Check your cache directory permissions?';
614 } else {
615 return true;
616 }
617 }
618 }