xref: /dokuwiki/lib/exe/js.php (revision bbca79d883e9dbd29f59556a7e12322da7e9b02a)
1<?php
2/**
3 * DokuWiki JavaScript creator
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9if(!defined('DOKU_INC')) define('DOKU_INC',dirname(__FILE__).'/../../');
10if(!defined('NOSESSION')) define('NOSESSION',true); // we do not use a session or authentication here (better caching)
11if(!defined('NL')) define('NL',"\n");
12if(!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT',1); // we gzip ourself here
13require_once(DOKU_INC.'inc/init.php');
14require_once(DOKU_INC.'inc/pageutils.php');
15require_once(DOKU_INC.'inc/httputils.php');
16require_once(DOKU_INC.'inc/io.php');
17require_once(DOKU_INC.'inc/JSON.php');
18
19// Main (don't run when UNIT test)
20if(!defined('SIMPLE_TEST')){
21    header('Content-Type: text/javascript; charset=utf-8');
22    js_out();
23}
24
25
26// ---------------------- functions ------------------------------
27
28/**
29 * Output all needed JavaScript
30 *
31 * @author Andreas Gohr <andi@splitbrain.org>
32 */
33function js_out(){
34    global $conf;
35    global $lang;
36    $edit  = (bool) $_REQUEST['edit'];   // edit or preview mode?
37    $write = (bool) $_REQUEST['write'];  // writable?
38
39    // The generated script depends on some dynamic options
40    $cache = getCacheName('scripts'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].$edit.'x'.$write,'.js');
41
42    // Array of needed files
43    $files = array(
44                DOKU_INC.'lib/scripts/helpers.js',
45                DOKU_INC.'lib/scripts/events.js',
46                DOKU_INC.'lib/scripts/cookie.js',
47                DOKU_INC.'lib/scripts/script.js',
48                DOKU_INC.'lib/scripts/tw-sack.js',
49                DOKU_INC.'lib/scripts/ajax.js',
50                DOKU_INC.'lib/scripts/index.js',
51             );
52    if($edit){
53        if($write){
54            $files[] = DOKU_INC.'lib/scripts/toolbar.js';
55            $files[] = DOKU_INC.'lib/scripts/edit.js';
56        }
57        $files[] = DOKU_INC.'lib/scripts/media.js';
58    }
59    $files[] = DOKU_TPLINC.'script.js';
60
61    // get possible plugin scripts
62    $plugins = js_pluginscripts();
63
64    // check cache age & handle conditional request
65    header('Cache-Control: public, max-age=3600');
66    header('Pragma: public');
67    if(js_cacheok($cache,array_merge($files,$plugins))){
68        http_conditionalRequest(filemtime($cache));
69        if($conf['allowdebug']) header("X-CacheUsed: $cache");
70
71        // finally send output
72        if ($conf['gzip_output'] && http_gzip_valid($cache)) {
73          header('Vary: Accept-Encoding');
74          header('Content-Encoding: gzip');
75          readfile($cache.".gz");
76        } else {
77          if (!http_sendfile($cache)) readfile($cache);
78        }
79
80        return;
81    } else {
82        http_conditionalRequest(time());
83    }
84
85    // start output buffering and build the script
86    ob_start();
87
88    // add some global variables
89    print "var DOKU_BASE   = '".DOKU_BASE."';";
90    print "var DOKU_TPL    = '".DOKU_TPL."';";
91
92    //FIXME: move thes into LANG
93    print "var alertText   = '".js_escape($lang['qb_alert'])."';";
94    print "var notSavedYet = '".js_escape($lang['notsavedyet'])."';";
95    print "var reallyDel   = '".js_escape($lang['del_confirm'])."';";
96
97    // load JS strings form plugins
98    $lang['js']['plugins'] = js_pluginstrings();
99
100    // load JS specific translations
101    $json = new JSON();
102    echo 'LANG = '.$json->encode($lang['js']).";\n";
103
104    // load files
105    foreach($files as $file){
106        echo "\n\n/* XXXXXXXXXX begin of $file XXXXXXXXXX */\n\n";
107        js_load($file);
108        echo "\n\n/* XXXXXXXXXX end of $file XXXXXXXXXX */\n\n";
109    }
110
111    // init stuff
112    js_runonstart("ajax_qsearch.init('qsearch__in','qsearch__out')");
113    js_runonstart("addEvent(document,'click',closePopups)");
114    js_runonstart('addTocToggle()');
115
116    if($edit){
117        // size controls
118        js_runonstart("initSizeCtl('size__ctl','wiki__text')");
119
120        if($write){
121            require_once(DOKU_INC.'inc/toolbar.php');
122            toolbar_JSdefines('toolbar');
123            js_runonstart("initToolbar('tool__bar','wiki__text',toolbar)");
124
125            // add pageleave check
126            js_runonstart("initChangeCheck('".js_escape($lang['notsavedyet'])."')");
127
128            // add lock timer
129            js_runonstart("locktimer.init(".($conf['locktime'] - 60).",'".js_escape($lang['willexpire'])."',".$conf['usedraft'].")");
130        }
131    }
132
133    // load plugin scripts (suppress warnings for missing ones)
134    foreach($plugins as $plugin){
135        if (@file_exists($plugin)) {
136          echo "\n\n/* XXXXXXXXXX begin of $plugin XXXXXXXXXX */\n\n";
137          js_load($plugin);
138          echo "\n\n/* XXXXXXXXXX end of $plugin XXXXXXXXXX */\n\n";
139        }
140    }
141
142    // load user script
143    @readfile(DOKU_CONF.'userscript.js');
144
145    // add scroll event and tooltip rewriting
146    js_runonstart('scrollToMarker()');
147    js_runonstart('focusMarker()');
148
149    // end output buffering and get contents
150    $js = ob_get_contents();
151    ob_end_clean();
152
153    // compress whitespace and comments
154    if($conf['compress']){
155        $js = js_compress($js);
156    }
157
158    $js .= "\n"; // https://bugzilla.mozilla.org/show_bug.cgi?id=316033
159
160    // save cache file
161    io_saveFile($cache,$js);
162    copy($cache,"compress.zlib://$cache.gz");
163
164    // finally send output
165    if ($conf['gzip_output']) {
166      header('Vary: Accept-Encoding');
167      header('Content-Encoding: gzip');
168      print gzencode($js,9,FORCE_GZIP);
169    } else {
170      print $js;
171    }
172}
173
174/**
175 * Load the given file, handle include calls and print it
176 *
177 * @author Andreas Gohr <andi@splitbrain.org>
178 */
179function js_load($file){
180    if(!@file_exists($file)) return;
181    static $loaded = array();
182
183    $data = io_readFile($file);
184    while(preg_match('#/\*\s*DOKUWIKI:include(_once)?\s+([\w\./]+)\s*\*/#',$data,$match)){
185        $ifile = $match[2];
186
187        // is it a include_once?
188        if($match[1]){
189            $base = basename($ifile);
190            if($loaded[$base]) continue;
191            $loaded[$base] = true;
192        }
193
194        if($ifile{0} != '/') $ifile = dirname($file).'/'.$ifile;
195
196        if(@file_exists($ifile)){
197            $idata = io_readFile($ifile);
198        }else{
199            $idata = '';
200        }
201        $data  = str_replace($match[0],$idata,$data);
202    }
203    echo $data;
204}
205
206/**
207 * Checks if a JavaScript Cache file still is valid
208 *
209 * @author Andreas Gohr <andi@splitbrain.org>
210 */
211function js_cacheok($cache,$files){
212    if($_REQUEST['purge']) return false; //support purge request
213
214    $ctime = @filemtime($cache);
215    if(!$ctime) return false; //There is no cache
216
217    // some additional files to check
218    $files = array_merge($files, getConfigFiles('main'));
219    $files[] = DOKU_CONF.'userscript.js';
220    $files[] = __FILE__;
221
222    // now walk the files
223    foreach($files as $file){
224        if(@filemtime($file) > $ctime){
225            return false;
226        }
227    }
228    return true;
229}
230
231/**
232 * Returns a list of possible Plugin Scripts (no existance check here)
233 *
234 * @author Andreas Gohr <andi@splitbrain.org>
235 */
236function js_pluginscripts(){
237    $list = array();
238    $plugins = plugin_list();
239    foreach ($plugins as $p){
240        $list[] = DOKU_PLUGIN."$p/script.js";
241    }
242    return $list;
243}
244
245/**
246 * Return an two-dimensional array with strings from the language file of each plugin.
247 *
248 * - $lang['js'] must be an array.
249 * - Nothing is returned for plugins without an entry for $lang['js']
250 *
251 * @author Gabriel Birke <birke@d-scribe.de>
252 */
253function js_pluginstrings()
254{
255    global $conf;
256    $pluginstrings = array();
257    $plugins = plugin_list();
258    foreach ($plugins as $p){
259        if (isset($lang)) unset($lang);
260        if (@file_exists(DOKU_PLUGIN."$p/lang/en/lang.php")) {
261            include DOKU_PLUGIN."$p/lang/en/lang.php";
262        }
263        if (isset($conf['lang']) && $conf['lang']!='en' && @file_exists(DOKU_PLUGIN."$p/lang/".$conf['lang']."/lang.php")) {
264            include DOKU_PLUGIN."$p/lang/".$conf['lang']."/lang.php";
265        }
266        if (isset($lang['js'])) {
267            $pluginstrings[$p] = $lang['js'];
268        }
269    }
270    return $pluginstrings;
271}
272
273/**
274 * Escapes a String to be embedded in a JavaScript call, keeps \n
275 * as newline
276 *
277 * @author Andreas Gohr <andi@splitbrain.org>
278 */
279function js_escape($string){
280    return str_replace('\\\\n','\\n',addslashes($string));
281}
282
283/**
284 * Adds the given JavaScript code to the window.onload() event
285 *
286 * @author Andreas Gohr <andi@splitbrain.org>
287 */
288function js_runonstart($func){
289    echo "addInitEvent(function(){ $func; });".NL;
290}
291
292/**
293 * Strip comments and whitespaces from given JavaScript Code
294 *
295 * This is a port of Nick Galbreath's python tool jsstrip.py which is
296 * released under BSD license. See link for original code.
297 *
298 * @author Nick Galbreath <nickg@modp.com>
299 * @author Andreas Gohr <andi@splitbrain.org>
300 * @link   http://code.google.com/p/jsstrip/
301 */
302function js_compress($s){
303    $s = ltrim($s);     // strip all initial whitespace
304    $s .= "\n";
305    $i = 0;             // char index for input string
306    $j = 0;             // char forward index for input string
307    $line = 0;          // line number of file (close to it anyways)
308    $slen = strlen($s); // size of input string
309    $lch  = '';         // last char added
310    $result = '';       // we store the final result here
311
312    // items that don't need spaces next to them
313    $chars = "^&|!+\-*\/%=\?:;,{}()<>% \t\n\r'\"[]";
314
315    while($i < $slen){
316        // skip all "boring" characters.  This is either
317        // reserved word (e.g. "for", "else", "if") or a
318        // variable/object/method (e.g. "foo.color")
319        while ($i < $slen && (strpos($chars,$s[$i]) === false) ){
320            $result .= $s{$i};
321            $i = $i + 1;
322        }
323
324        $ch = $s{$i};
325        // multiline comments (keeping IE conditionals)
326        if($ch == '/' && $s{$i+1} == '*' && $s{$i+2} != '@'){
327            $endC = strpos($s,'*/',$i+2);
328            if($endC === false) trigger_error('Found invalid /*..*/ comment', E_USER_ERROR);
329            $i = $endC + 2;
330            continue;
331        }
332
333        // singleline
334        if($ch == '/' && $s{$i+1} == '/'){
335            $endC = strpos($s,"\n",$i+2);
336            if($endC === false) trigger_error('Invalid comment', E_USER_ERROR);
337            $i = $endC;
338            continue;
339        }
340
341        // tricky.  might be an RE
342        if($ch == '/'){
343            // rewind, skip white space
344            $j = 1;
345            while($s{$i-$j} == ' '){
346                $j = $j + 1;
347            }
348            if( ($s{$i-$j} == '=') || ($s{$i-$j} == '(') ){
349                // yes, this is an re
350                // now move forward and find the end of it
351                $j = 1;
352                while($s{$i+$j} != '/'){
353                    while( ($s{$i+$j} != '\\') && ($s{$i+$j} != '/')){
354                        $j = $j + 1;
355                    }
356                    if($s{$i+$j} == '\\') $j = $j + 2;
357                }
358                $result .= substr($s,$i,$j+1);
359                $i = $i + $j + 1;
360                continue;
361            }
362        }
363
364        // double quote strings
365        if($ch == '"'){
366            $j = 1;
367            while( $s{$i+$j} != '"' && ($i+$j < $slen)){
368                if( $s{$i+$j} == '\\' && ($s{$i+$j+1} == '"' || $s{$i+$j+1} == '\\') ){
369                    $j += 2;
370                }else{
371                    $j += 1;
372                }
373            }
374            $result .= substr($s,$i,$j+1);
375            $i = $i + $j + 1;
376            continue;
377        }
378
379        // single quote strings
380        if($ch == "'"){
381            $j = 1;
382            while( $s{$i+$j} != "'" && ($i+$j < $slen)){
383                if( $s{$i+$j} == '\\' && ($s{$i+$j+1} == "'" || $s{$i+$j+1} == '\\') ){
384                    $j += 2;
385                }else{
386                    $j += 1;
387                }
388            }
389            $result .= substr($s,$i,$j+1);
390            $i = $i + $j + 1;
391            continue;
392        }
393
394        // whitespaces
395        if( $ch == ' ' || $ch == "\r" || $ch == "\n" || $ch == "\t" ){
396            // leading spaces
397            if($i+1 < $slen && (strpos($chars,$s[$i+1]) !== false)){
398                $i = $i + 1;
399                continue;
400            }
401            // trailing spaces
402            //  if this ch is space AND the last char processed
403            //  is special, then skip the space
404            $lch = substr($result,-1);
405            if($lch && (strpos($chars,$lch) !== false)){
406                $i = $i + 1;
407                continue;
408            }
409            // else after all of this convert the "whitespace" to
410            // a single space.  It will get appended below
411            $ch = ' ';
412        }
413
414        // other chars
415        $result .= $ch;
416        $i = $i + 1;
417    }
418
419    return trim($result);
420}
421
422//Setup VIM: ex: et ts=4 enc=utf-8 :
423?>
424