Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/**
4
 * Combines multiple javascript files and serve up as gzip if possible.
5
 * Allowable scripts and script dependencies can be specified in a "packages.php" file
6
 * with the following format. This "packages.php" is optional, if absent the filenames
7
 * without ".js" extension are used.
8
 *
9
 * <code>
10
 * <?php
11
 *  $packages = array(
12
 *     'package1' => array('file1.js', 'file2.js'),
13
 *     'package2' => array('file3.js', 'file4.js'));
14
 *
15
 *  $dependencies = array(
16
 *     'package1' => array('package1'),
17
 *     'package2' => array('package1', 'package2')); //package2 requires package1 first.
18
 * </code>
19
 *
20
 * To serve up 'package1', specify the url, a maxium of 25 packages separated with commas is allows.
21
 *
22
 * clientscripts.php?js=package1
23
 *
24
 * for 'package2' (automatically resolves 'package1') dependency
25
 *
26
 * clientscripts.php?js=package2
27
 *
28
 * The scripts comments are removed and whitespaces removed appropriately. The
29
 * scripts may be served as zipped if the browser and php server allows it. Cache
30
 * headers are also sent to inform the browser and proxies to cache the file.
31
 * Moreover, the post-processed (comments removed and zipped) are saved in this
32
 * current directory for the next requests.
33
 *
34
 * If the url contains the parameter "mode=debug", the comments are not removed
35
 * and headers invalidating the cache are sent. In debug mode, the script can still
36
 * be zipped if the browser and server supports it.
37
 *
38
 * E.g. clientscripts.php?js=package2&mode=debug
39
 *
40
 * @link http://www.pradosoft.com/
41
 * @copyright Copyright &copy; 2007 PradoSoft
42
 * @license http://www.pradosoft.com/license/
43
 * @author Wei Zhuo<weizhuo[at]gmail[dot]com>
44
 * @version $Id$
45
 * @package System.Web.Javascripts
46
 * @since 3.1
47
 */
48
 
49
//run script for as long as needed
50
set_time_limit(0);
51
 
52
//set error_reporting directive
53
@error_reporting(E_ERROR | E_WARNING | E_PARSE);
54
 
55
function get_client_script_files()
56
{
57
	$package_file = dirname(__FILE__).'/packages.php';
58
	if(is_file($package_file))
59
		return get_package_files($package_file, get_script_requests());
60
	else
61
		return get_javascript_files(get_script_requests());
62
}
63
 
64
/**
65
 * @param array list of requested libraries
66
 */
67
function get_script_requests($max=25)
68
{
69
	$param = isset($_GET['js']) ? $_GET['js'] : '';
70
	if(preg_match('/([a-zA-z0-9\-_])+(,[a-zA-z0-9\-_]+)*/', $param))
71
		return array_unique(explode(',', $param, $max));
72
	return array();
73
}
74
 
75
/**
76
 * @param string packages.php filename
77
 * @param array packages requests
78
 */
79
function get_package_files($package_file, $request)
80
{
81
	list($packages, $dependencies) = include($package_file);
82
	if(!(is_array($packages) && is_array($dependencies)))
83
	{
84
		error_log('Prado client script: invalid package file '.$package_file);
85
		return array();
86
	}
87
	$result = array();
88
	$found = array();
89
	foreach($request as $library)
90
	{
91
		if(isset($dependencies[$library]))
92
		{
93
			foreach($dependencies[$library] as $dep)
94
			{
95
				if(isset($found[$dep]))
96
					continue;
97
				$result = array_merge($result, (array)$packages[$dep]);
98
				$found[$dep]=true;
99
			}
100
		}
101
		else
102
			error_log('Prado client script: no such javascript library "'.$library.'"');
103
	}
104
	return $result;
105
}
106
 
107
/**
108
 * @param string requested javascript files
109
 * @array array list of javascript files.
110
 */
111
function get_javascript_files($request)
112
{
113
	$result = array();
114
	foreach($request as $file)
115
		$result[] = $file.'.js';
116
	return $result;
117
}
118
 
119
/**
120
 * @param array list of javascript files.
121
 * @return string combined the available javascript files.
122
 */
123
function combine_javascript($files)
124
{
125
	$content = '';
126
	$base = dirname(__FILE__);
127
	foreach($files as $file)
128
	{
129
		$filename = $base.'/'.$file;
130
		if(is_file($filename)) //relies on get_client_script_files() for security
131
			$content .= "\x0D\x0A".file_get_contents($filename); //add CR+LF
132
		else
133
			error_log('Prado client script: missing file '.$filename);
134
	}
135
	return $content;
136
}
137
 
138
/**
139
 * @param string javascript code
140
 * @param array files names
141
 * @return string javascript code without comments.
142
 */
143
function minify_javascript($content, $files)
144
{
145
	return JSMin::minify($content);
146
}
147
 
148
/**
149
 * @param boolean true if in debug mode.
150
 */
151
function is_debug_mode()
152
{
153
	return isset($_GET['mode']) && $_GET['mode']==='debug';
154
}
155
 
156
/**
157
 * @param string javascript code
158
 * @param string gzip code
159
 */
160
function gzip_content($content)
161
{
162
	return gzencode($content, 9, FORCE_GZIP);
163
}
164
 
165
/**
166
 * @param string javascript code.
167
 * @param string filename
168
 */
169
function save_javascript($content, $filename)
170
{
171
	file_put_contents($filename, $content);
172
	if(supports_gzip_encoding())
173
		file_put_contents($filename.'.gz', gzip_content($content));
174
}
175
 
176
/**
177
 * @param string comprssed javascript file to be read
178
 * @param string javascript code, null if file is not found.
179
 */
180
function get_saved_javascript($filename)
181
{
182
	$fn=$filename;
183
	if(supports_gzip_encoding())
184
		$fn .= '.gz';
185
	if(!is_file($fn))
186
		save_javascript(get_javascript_code(true), $filename);
187
	return file_get_contents($fn);
188
}
189
 
190
/**
191
 * @return string compressed javascript file name.
192
 */
193
function compressed_js_filename()
194
{
195
	$files = get_client_script_files();
196
	if(count($files) > 0)
197
	{
198
		$filename = sprintf('%x',crc32(implode(',',($files))));
199
		return dirname(__FILE__).'/clientscript_'.$filename.'.js';
200
	}
201
}
202
 
203
/**
204
 * @param boolean true to strip comments from javascript code
205
 * @return string javascript code
206
 */
207
function get_javascript_code($minify=false)
208
{
209
	$files = get_client_script_files();
210
	if(count($files) > 0)
211
	{
212
		$content = combine_javascript($files);
213
		if($minify)
214
			return minify_javascript($content, $files);
215
		else
216
			return $content;
217
	}
218
}
219
 
220
/**
221
 * Prints headers to serve javascript
222
 */
223
function print_headers()
224
{
225
	$expiresOffset = is_debug_mode() ? -10000 : 3600 * 24 * 10; //no cache
226
	header("Content-type: text/javascript");
227
	header("Vary: Accept-Encoding");  // Handle proxies
228
	header("Expires: " . @gmdate("D, d M Y H:i:s", @time() + $expiresOffset) . " GMT");
229
	if(($enc = supports_gzip_encoding()) !== null)
230
		header("Content-Encoding: " . $enc);
231
}
232
 
233
/**
234
 * @return string 'x-gzip' or 'gzip' if php supports gzip and browser supports gzip response, null otherwise.
235
 */
236
function supports_gzip_encoding()
237
{
238
	if(isset($_GET['gzip']) && $_GET['gzip']==='false')
239
		return false;
240
 
241
	if (isset($_SERVER['HTTP_ACCEPT_ENCODING']))
242
	{
243
		$encodings = explode(',', strtolower(preg_replace("/\s+/", "", $_SERVER['HTTP_ACCEPT_ENCODING'])));
244
		$allowsZipEncoding = in_array('gzip', $encodings) || in_array('x-gzip', $encodings) || isset($_SERVER['---------------']);
245
		$hasGzip = function_exists('ob_gzhandler');
246
		$noZipBuffer = !ini_get('zlib.output_compression');
247
		$noZipBufferHandler = ini_get('output_handler') != 'ob_gzhandler';
248
 
249
		if ( $allowsZipEncoding && $hasGzip && $noZipBuffer && $noZipBufferHandler)
250
			$enc = in_array('x-gzip', $encodings) ? "x-gzip" : "gzip";
251
		return $enc;
252
	}
253
}
254
 
255
/**
256
 * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
257
 *
258
 * This is pretty much a direct port of jsmin.c to PHP with just a few
259
 * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
260
 * outputs to stdout, this library accepts a string as input and returns another
261
 * string as output.
262
 *
263
 * PHP 5 or higher is required.
264
 *
265
 * Permission is hereby granted to use this version of the library under the
266
 * same terms as jsmin.c, which has the following license:
267
 *
268
 * --
269
 * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
270
 *
271
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
272
 * this software and associated documentation files (the "Software"), to deal in
273
 * the Software without restriction, including without limitation the rights to
274
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
275
 * of the Software, and to permit persons to whom the Software is furnished to do
276
 * so, subject to the following conditions:
277
 *
278
 * The above copyright notice and this permission notice shall be included in all
279
 * copies or substantial portions of the Software.
280
 *
281
 * The Software shall be used for Good, not Evil.
282
 *
283
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
284
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
285
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
286
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
287
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
288
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
289
 * SOFTWARE.
290
 * --
291
 *
292
 * @package JSMin
293
 * @author Ryan Grove <ryan@wonko.com>
294
 * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
295
 * @copyright 2007 Ryan Grove <ryan@wonko.com> (PHP port)
296
 * @license http://opensource.org/licenses/mit-license.php MIT License
297
 * @version 1.1.0 (2007-06-01)
298
 * @link http://code.google.com/p/jsmin-php/
299
 */
300
 
301
class JSMin {
302
  const ORD_LF    = 10;
303
  const ORD_SPACE = 32;
304
 
305
  protected $a           = '';
306
  protected $b           = '';
307
  protected $input       = '';
308
  protected $inputIndex  = 0;
309
  protected $inputLength = 0;
310
  protected $lookAhead   = null;
311
  protected $output      = array();
312
 
313
  // -- Public Static Methods --------------------------------------------------
314
 
315
  public static function minify($js) {
316
    $jsmin = new JSMin($js);
317
    return $jsmin->min();
318
  }
319
 
320
  // -- Public Instance Methods ------------------------------------------------
321
 
322
  public function __construct($input) {
323
    $this->input       = str_replace("\r\n", "\n", $input);
324
    $this->inputLength = strlen($this->input);
325
  }
326
 
327
  // -- Protected Instance Methods ---------------------------------------------
328
 
329
  protected function action($d) {
330
    switch($d) {
331
      case 1:
332
        $this->output[] = $this->a;
333
 
334
      case 2:
335
        $this->a = $this->b;
336
 
337
        if ($this->a === "'" || $this->a === '"') {
338
          for (;;) {
339
            $this->output[] = $this->a;
340
            $this->a        = $this->get();
341
 
342
            if ($this->a === $this->b) {
343
              break;
344
            }
345
 
346
            if (ord($this->a) <= self::ORD_LF) {
347
              throw new JSMinException('Unterminated string literal.');
348
            }
349
 
350
            if ($this->a === '\\') {
351
              $this->output[] = $this->a;
352
              $this->a        = $this->get();
353
            }
354
          }
355
        }
356
 
357
      case 3:
358
        $this->b = $this->next();
359
 
360
        if ($this->b === '/' && (
361
            $this->a === '(' || $this->a === ',' || $this->a === '=' ||
362
            $this->a === ':' || $this->a === '[' || $this->a === '!' ||
363
            $this->a === '&' || $this->a === '|' || $this->a === '?')) {
364
 
365
          $this->output[] = $this->a;
366
          $this->output[] = $this->b;
367
 
368
          for (;;) {
369
            $this->a = $this->get();
370
 
371
            if ($this->a === '/') {
372
              break;
373
            }
374
            elseif ($this->a === '\\') {
375
              $this->output[] = $this->a;
376
              $this->a        = $this->get();
377
            }
378
            elseif (ord($this->a) <= self::ORD_LF) {
379
              throw new JSMinException('Unterminated regular expression '.
380
                  'literal.');
381
            }
382
 
383
            $this->output[] = $this->a;
384
          }
385
 
386
          $this->b = $this->next();
387
        }
388
    }
389
  }
390
 
391
  protected function get() {
392
    $c = $this->lookAhead;
393
    $this->lookAhead = null;
394
 
395
    if ($c === null) {
396
      if ($this->inputIndex < $this->inputLength) {
397
        $c = $this->input[$this->inputIndex];
398
        $this->inputIndex += 1;
399
      }
400
      else {
401
        $c = null;
402
      }
403
    }
404
 
405
    if ($c === "\r") {
406
      return "\n";
407
    }
408
 
409
    if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
410
      return $c;
411
    }
412
 
413
    return ' ';
414
  }
415
 
416
  protected function isAlphaNum($c) {
417
    return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
418
  }
419
 
420
  protected function min() {
421
    $this->a = "\n";
422
    $this->action(3);
423
 
424
    while ($this->a !== null) {
425
      switch ($this->a) {
426
        case ' ':
427
          if ($this->isAlphaNum($this->b)) {
428
            $this->action(1);
429
          }
430
          else {
431
            $this->action(2);
432
          }
433
          break;
434
 
435
        case "\n":
436
          switch ($this->b) {
437
            case '{':
438
            case '[':
439
            case '(':
440
            case '+':
441
            case '-':
442
              $this->action(1);
443
              break;
444
 
445
            case ' ':
446
              $this->action(3);
447
              break;
448
 
449
            default:
450
              if ($this->isAlphaNum($this->b)) {
451
                $this->action(1);
452
              }
453
              else {
454
                $this->action(2);
455
              }
456
          }
457
          break;
458
 
459
        default:
460
          switch ($this->b) {
461
            case ' ':
462
              if ($this->isAlphaNum($this->a)) {
463
                $this->action(1);
464
                break;
465
              }
466
 
467
              $this->action(3);
468
              break;
469
 
470
            case "\n":
471
              switch ($this->a) {
472
                case '}':
473
                case ']':
474
                case ')':
475
                case '+':
476
                case '-':
477
                case '"':
478
                case "'":
479
                  $this->action(1);
480
                  break;
481
 
482
                default:
483
                  if ($this->isAlphaNum($this->a)) {
484
                    $this->action(1);
485
                  }
486
                  else {
487
                    $this->action(3);
488
                  }
489
              }
490
              break;
491
 
492
            default:
493
              $this->action(1);
494
              break;
495
          }
496
      }
497
    }
498
 
499
    return implode('', $this->output);
500
  }
501
 
502
  protected function next() {
503
    $c = $this->get();
504
 
505
    if ($c === '/') {
506
      switch($this->peek()) {
507
        case '/':
508
          for (;;) {
509
            $c = $this->get();
510
 
511
            if (ord($c) <= self::ORD_LF) {
512
              return $c;
513
            }
514
          }
515
 
516
        case '*':
517
          $this->get();
518
 
519
          for (;;) {
520
            switch($this->get()) {
521
              case '*':
522
                if ($this->peek() === '/') {
523
                  $this->get();
524
                  return ' ';
525
                }
526
                break;
527
 
528
              case null:
529
                throw new JSMinException('Unterminated comment.');
530
            }
531
          }
532
 
533
        default:
534
          return $c;
535
      }
536
    }
537
 
538
    return $c;
539
  }
540
 
541
  protected function peek() {
542
    $this->lookAhead = $this->get();
543
    return $this->lookAhead;
544
  }
545
}
546
 
547
// -- Exceptions ---------------------------------------------------------------
548
class JSMinException extends Exception {}
549
 
550
 
551
/************** OUTPUT *****************/
552
 
553
if(count(get_script_requests()) > 0)
554
{
555
	if(is_debug_mode())
556
	{
557
		if(($code = get_javascript_code()) !== null)
558
		{
559
			print_headers();
560
			echo supports_gzip_encoding() ? gzip_content($code) : $code;
561
		}
562
	}
563
	else
564
	{
565
		if(($filename = compressed_js_filename()) !== null)
566
		{
567
			if(!is_file($filename))
568
				save_javascript(get_javascript_code(true), $filename);
569
			print_headers();
570
			echo get_saved_javascript($filename);
571
		}
572
	}
573
}
574
 
575
?>