* escaped a few more values
[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 // check if we are being invoked in MediaWiki context or stand alone usage:
7 if ( !defined( 'MEDIAWIKI' ) ){
8 // load noMediaWiki helper:
9 require_once( realpath( dirname( __FILE__ ) ) . '/php/noMediaWikiConfig.php' );
10
11 // run the main action:
12 $myScriptLoader = new jsScriptLoader();
13 // preset request values via normal $_GET operation:
14 $myScriptLoader->doScriptLoader();
15 } else {
16 $wgExtensionMessagesFiles['mwEmbed'] = realpath( dirname( __FILE__ ) ) . '/php/mwEmbed.i18n.php';
17 }
18
19 // setup page output hook
20 class jsScriptLoader {
21 var $jsFileList = array();
22 var $jsout = '';
23 var $rKey = ''; // the request key
24 var $error_msg = '';
25 var $debug = false;
26 var $jsvarurl = false; // if we should include generated js (special class '-')
27 var $doProcReqFlag = true;
28
29 function doScriptLoader(){
30 global $wgJSAutoloadClasses, $wgJSAutoloadLocalClasses, $wgEnableScriptLoaderJsFile, $IP,
31 $wgEnableScriptMinify, $wgUseFileCache;
32
33 // process the request
34 $this->procRequestVars();
35
36 // if cache is on and file is present grab it from there:
37 if( $wgUseFileCache && !$this->debug ) {
38 // setup file cache obj:
39 $this->sFileCache = new simpleFileCache( $this->rKey );
40 if( $this->sFileCache->isFileCached() ){
41 // just output headers so we can use php "efficient" readfile
42 $this->outputJsHeaders();
43 $this->sFileCache->outputFromFileCache();
44 die();
45 }
46 }
47
48 // setup script loader header info
49 $this->jsout .= 'var mwSlScript = "' . htmlspecialchars( $_SERVER['SCRIPT_NAME'] ) . '";' . "\n";
50 $this->jsout .= 'var mwSlGenISODate = "' . date( 'c' ) . '";' ."\n";
51 $this->jsout .= 'var mwSlURID = "' . htmlspecialchars( $this->urid ) . '";' ."\n";
52 // Build the output:
53 // swap in the appropriate language per js_file
54 foreach( $this->jsFileList as $classKey => $file_name ){
55
56 // special case: - title classes:
57 if( substr( $classKey, 0, 3 ) == 'WT:' ){
58 global $wgUser;
59 // get just the tile part:
60 $title_block = substr( $classKey, 3 );
61 if( $title_block[0] == '-' && strpos( $title_block, '|' ) !== false ){
62 // special case of "-" title with skin
63 $parts = explode( '|', $title_block );
64 $title = array_shift( $parts );
65 foreach( $parts as $tparam ){
66 list( $key, $val ) = explode( '=', $tparam );
67 if( $key == 'useskin' ){
68 $skin = $val;
69 }
70 }
71 $sk = $wgUser->getSkin();
72 // make sure the skin name is valid
73 $skinNames = Skin::getSkinNames();
74 // get the lower case skin name (array keys)
75 $skinNames = array_keys( $skinNames );
76 if( in_array( strtolower( $skin ), $skinNames ) ){
77 $this->jsout .= $sk->generateUserJs( $skin ) . "\n";
78 // success continue:
79 continue;
80 }
81 } else {
82 //make sure the wiki title ends with .js
83 if( substr( $title_block, -3 ) != '.js'){
84 $this->error_msg .= 'WikiTitle includes should end with .js';
85 continue;
86 }
87 // it's a wikiTitle append the output of the wikitext:
88 $t = Title::newFromText( $title_block );
89 $a = new Article( $t );
90 // only get content if the page is not empty:
91 if( $a->getID() !== 0 ){
92 $this->jsout .= $a->getContent() . "\n";
93 }
94 continue;
95 }
96 }
97 //dealing with files::
98 //check that the filename ends with .js and does not include ../ traversing
99 if( substr( $file_name, -3 ) != '.js'){
100 $this->error_msg .= "\nError file name must end with .js: ". htmlspecialchars( $file_name ) . " \n ";
101 continue;
102 }
103 if( strpos($file_name, '../') !== false ){
104 $this->error_msg .= "\nError file name must not traverse paths: ". htmlspecialchars( $file_name ) . " \n ";
105 continue;
106 }
107
108 if( trim( $file_name ) != '' ){
109 // if in debug add a comment with the file name:
110 if( $this->debug )
111 $this->jsout .= "\n/**
112 * File: ". htmlspecialchars( $file_name ) ."
113 */\n";
114 $this->jsout .= ( $this->doProccessJsFile( $file_name ) ) . "\n";
115 }
116 }
117
118 // check if we should minify :
119 if( $wgEnableScriptMinify && !$this->debug ){
120 // do the minification and output
121 $this->jsout = JSMin::minify( $this->jsout);
122 }
123 // save to the file cache:
124 if( $wgUseFileCache && !$this->debug ) {
125 $status = $this->sFileCache->saveToFileCache( $this->jsout );
126 if( $status !== true )
127 $this->error_msg.= $status;
128 }
129 // check for error msg:
130 if( $this->error_msg != ''){
131 echo 'alert(\'Error With ScriptLoader.php ::' . str_replace( "\n", '\'+"\n"+'."\n'", $this->error_msg ) . '\');';
132 echo trim( $this->jsout );
133 } else {
134 // all good lets output cache forever headers:
135 $this->outputJsWithHeaders();
136 }
137 }
138
139 function outputJsHeaders(){
140 global $wgJsMimeType;
141 // output js mime type:
142 header( 'Content-type: ' . $wgJsMimeType );
143 header( 'Pragma: public' );
144 // cache forever:
145 // (the point is we never have to revalidate since we should always change the request url based on the svn or article version)
146 $one_year = 60*60*24*365;
147 header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $one_year ) . " GM" );
148 }
149
150 function outputJsWithHeaders(){
151 global $wgUseGzip;
152 $this->outputJsHeaders();
153 if( $wgUseGzip ) {
154 if( wfClientAcceptsGzip() ) {
155 header( 'Content-Encoding: gzip' );
156 echo gzencode( $this->jsout );
157 } else {
158 echo $this->jsout;
159 }
160 } else {
161 echo $this->jsout;
162 }
163 }
164
165 /**
166 * updates the proc Request
167 */
168 function procRequestVars(){
169 global $wgContLanguageCode, $wgEnableScriptMinify, $wgJSAutoloadClasses,
170 $wgJSAutoloadLocalClasses, $wgStyleVersion, $wgEnableScriptLoaderJsFile;
171
172 // set debug flag:
173 if( ( isset( $_GET['debug'] ) && $_GET['debug'] == 'true' ) || ( isset( $wgEnableScriptDebug ) && $wgEnableScriptDebug == true ) ){
174 $this->debug = true;
175 }
176
177 // set the urid: (be sure to escape it as it goes into our js output)
178 if( isset( $_GET['urid'] ) && $_GET['urid'] !=''){
179 $this->urid = htmlspecialchars( $_GET['urid'] );
180 } else {
181 // just give it the current style sheet id:
182 // @@todo read the svn version number
183 $this->urid = $wgStyleVersion;
184 }
185
186 $reqClassList = false;
187 if( isset( $_GET['class'] ) && $_GET['class'] != '' ){
188 $reqClassList = explode( ',', $_GET['class'] );
189 }
190
191 // check for the requested classes
192 if( $reqClassList ){
193 // clean the class list and populate jsFileList
194 foreach( $reqClassList as $reqClass ){
195 if( trim( $reqClass ) != '' ){
196 // check for special case '-' class for user generated js
197 if( substr( $reqClass, 0, 3 ) == 'WT:' ){
198 $this->jsFileList[$reqClass] = true;
199 $this->rKey .= $reqClass;
200 $this->jsvarurl = true;
201 continue;
202 }
203
204 $reqClass = preg_replace("/[^A-Za-z0-9_\-\.]/", '', $reqClass );
205
206 if( isset( $wgJSAutoloadLocalClasses[$reqClass] ) ){
207 $this->jsFileList[$reqClass] = $wgJSAutoloadLocalClasses[$reqClass];
208 $this->rKey.= $reqClass;
209 } else if( isset( $wgJSAutoloadClasses[$reqClass] ) ) {
210 $this->jsFileList[$reqClass] = $wgJSAutoloadClasses[$reqClass];
211 $this->rKey.= $reqClass;
212 } else {
213 $this->error_msg.= 'Requested class: ' . htmlspecialchars( $reqClass ) . ' not found' . "\n";
214 }
215 }
216 }
217 }
218
219 // check for requested files if enabled:
220 if( $wgEnableScriptLoaderJsFile ){
221 if( isset( $_GET['files'] ) ){
222 $reqFileList = explode( ',', isset( $_GET['files'] ) );
223 // clean the file list and populate jsFileList
224 foreach( $reqFileList as $reqFile ){
225 // no jumping dirs:
226 $reqFile = str_replace( '../', '', $reqFile );
227 // only allow alphanumeric underscores periods and ending with .js
228 $reqFile = ereg_replace( "[^A-Za-z0-9_\-\/\.]", '', $reqFile );
229 if( substr( $reqFile, -3 ) == '.js' ){
230 // don't add it twice:
231 if( !in_array( $reqFile, $jsFileList ) ) {
232 $this->jsFileList[] = $IP . $reqFile;
233 $this->rKey.= $reqFile;
234 }
235 } else {
236 $this->error_msg.= 'Not valid requsted JavaScript file' . "\n";
237 }
238 }
239 }
240 }
241
242 // add the language code to the rKey:
243 $this->rKey .= '_' . $wgContLanguageCode;
244
245 // add the unique rid to the rKey
246 $this->rKey .= $this->urid;
247
248 // add a min flag:
249 if( $wgEnableScriptMinify ){
250 $this->rKey.= '_min';
251 }
252 }
253
254 function doProccessJsFile( $file_name ){
255 global $IP, $wgEnableScriptLocalization, $IP;
256
257 // load the file:
258 $str = @file_get_contents( "{$IP}/{$file_name}" );
259
260 if( $str === false ){
261 // @@todo check php error level (don't want to expose paths if errors are hidden)
262 $this->error_msg.= 'Requested File: ' . htmlspecialchars( $file_name ) . ' could not be read' . "\n";
263 return '';
264 }
265 $this->cur_file = $file_name;
266
267 // strip out js_log debug lines not much luck with this regExp yet:
268 //if( !$this->debug )
269 // $str = preg_replace('/\n\s*js_log\s*\([^\)]([^;]|\n])*;/', "\n", $str);
270
271 // do language swap
272 if( $wgEnableScriptLocalization )
273 $str = preg_replace_callback(
274 '/loadGM\s*\(\s*{(.*)}\s*\)\s*/siU', // @@todo fix: will break down if someone does }) in their msg text
275 array( $this, 'languageMsgReplace' ),
276 $str
277 );
278
279 return $str;
280 }
281
282 function languageMsgReplace( $jvar ){
283 if( !isset( $jvar[1] ) )
284 return;
285
286 $jmsg = json_decode( '{' . $jvar[1] . '}', true );
287 // do the language lookup:
288 if( $jmsg ){
289 foreach( $jmsg as $msgKey => $default_en_value ){
290 $jmsg[$msgKey] = wfMsgNoTrans( $msgKey );
291 }
292 //return the updated loadGM json with fixed new lines:
293 return 'loadGM( ' . json_encode( $jmsg ) . ')';
294 } else {
295 $this->error_msg.= "Could not parse JSON language msg in File:\n" .
296 htmlspecialchars ( $this->cur_file ) . "\n";
297 }
298 // could not parse json (throw error?)
299 return $jvar[0];
300 }
301 }
302
303 //a simple version of HTMLFileCache (@@todo abstract shared pieces)
304 class simpleFileCache {
305 var $mFileCache;
306 var $filename = null;
307 var $rKey = null;
308
309 public function __construct( &$rKey ) {
310 $this->rKey = $rKey;
311 $this->filename = $this->fileCacheName(); // init name
312 }
313
314 public function fileCacheName() {
315 global $wgUseGzip;
316 if( !$this->mFileCache ) {
317 global $wgFileCacheDirectory;
318
319 $hash = md5( $this->rKey );
320 # Avoid extension confusion
321 $key = str_replace( '.', '%2E', urlencode( $this->rKey ) );
322
323 $hash1 = substr( $hash, 0, 1 );
324 $hash2 = substr( $hash, 0, 2 );
325 $this->mFileCache = "{$wgFileCacheDirectory}/{$hash1}/{$hash2}/{$this->rKey}.js";
326
327 if( $wgUseGzip )
328 $this->mFileCache .= '.gz';
329
330 wfDebug( " fileCacheName() - {$this->mFileCache}\n" );
331 }
332 return $this->mFileCache;
333 }
334
335 public function isFileCached() {
336 return file_exists( $this->filename );
337 }
338
339 public function outputFromFileCache(){
340 global $wgUseGzip;
341 if( $wgUseGzip ) {
342 if( wfClientAcceptsGzip() ) {
343 header( 'Content-Encoding: gzip' );
344 readfile( $this->filename );
345 } else {
346 /* Send uncompressed (check if fileCache is in compressed state (ends with .gz)
347 * (unlikely to execute this since $wgUseGzip would have created a new file above.. but just in case:
348 */
349 if( substr( $this->filename, -3 ) == '.gz' ){
350 readgzfile( $this->filename );
351 } else {
352 readfile( $this->filename );
353 }
354 }
355 } else {
356 // just output the file
357 readfile( $this->filename );
358 }
359 //return true
360 return true;
361 }
362
363 public function saveToFileCache( &$text ) {
364 global $wgUseFileCache, $wgUseGzip;
365 if( !$wgUseFileCache ) {
366 return 'Error: Called saveToFileCache with $wgUseFileCache off';
367 }
368 if( strcmp( $text, '' ) == 0 ) return 'saveToFileCache: empty output file';
369
370 // check the directories if we could not create them error out:
371 $status = $this->checkCacheDirs();
372
373 if( $wgUseGzip ){
374 $outputText = gzencode( trim( $text ) );
375 } else {
376 $outputText = trim( $text );
377 }
378
379 if( $status !== true )
380 return $status;
381 $f = fopen( $this->filename, 'w' );
382 if( $f ) {
383 fwrite( $f, $outputText );
384 fclose( $f );
385 } else {
386 return 'Could not open file for writing. Check your cache directory permissions?';
387 }
388 return true;
389 }
390
391 protected function checkCacheDirs() {
392 $mydir2 = substr( $this->filename, 0, strrpos( $this->filename, '/' ) ); # subdirectory level 2
393 $mydir1 = substr( $mydir2, 0, strrpos( $mydir2, '/' ) ); # subdirectory level 1
394
395 if( wfMkdirParents( $mydir1 ) === false || wfMkdirParents( $mydir2 ) === false ){
396 return 'Could not create cache directory. Check your cache directory permissions?';
397 } else {
398 return true;
399 }
400 }
401 }