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