Xataface  2.0alpha2
Xataface Application Framework
 All Data Structures Namespaces Files Functions Variables Groups Pages
JavascriptTool.php
Go to the documentation of this file.
1 <?php
2 //if ( !class_exists('Dataface_JavascriptTool')){
3 require_once 'Dataface/CSSTool.php';
8 
13  private static $instance=null;
14 
20  private $includePath = array();
21 
28  private $scripts = array();
29 
34  private $included = array();
35 
40  private $css = null;
41 
45  private $minify = true;
46 
50  private $useCache = true;
51 
55  private $dependencies = array();
56 
61  private $dependencyContents = array();
62 
63 
64  private $cssIncludes = array();
65 
66 
74  private $ignoreScripts = array();
75 
76 
77  public function ignore($script){
78  $this->ignoreScripts[$script] = 1;
79  if ( isset($this->scripts[$script]) ) unset($this->scripts[$script]);
80  }
81 
82  public function isIgnored($script){
83  return @$this->ignoreScripts[$script];
84  }
85 
86  public function ignoreCss($stylesheet){
87  $this->css->ignore($stylesheet);
88  }
89 
90 
95  public static function getInstance($type = null){
96  if ( !isset($type) ){
97  if ( isset(self::$instance) ){
98  $type = get_class(self::$instance);
99  } else {
100  $type = 'Dataface_JavascriptTool';
101  }
102 
103 
104  }
105  if ( !isset(self::$instance) or get_class(self::$instance) != $type ){
106  self::$instance = new $type;
107  }
108  return self::$instance;
109  }
110 
114  public function __construct(){
115  $this->css = new Dataface_CSSTool();
116 
117  }
118 
123  public function setMinify($minify){
124  $this->minify = $minify;
125  }
126 
127 
132  public function getMinify(){
133  return $this->minify;
134  }
135 
139  public function setUseCache($cache){
140  $this->useCache = $cache;
141  }
142 
146  public function getUseCache(){
147  return $this->useCache;
148  }
149 
155  public function mergeCSSPaths(){
157  foreach ($this->css->getPaths() as $k=>$v){
158  $this->css->removePath($k);
159  }
160  foreach ($css->getPaths() as $k=>$v){
161  $this->css->addPath($k, $v);
162  }
163  }
164 
171  public function getScripts(){
172  return $this->scripts;
173  }
174 
175  public function clearScripts(){
176  foreach ($this->scripts as $k=>$v){
177  unset($this->scripts[$k]);
178  }
179  }
180 
181  public function copyTo(Dataface_JavascriptTool $target){
182  foreach ($this->includePath as $key=>$val){
183  $target->addPath($key, $val);
184  }
185 
186  foreach ($this->scripts as $key=>$val){
187  $target->import($key);
188  }
189  $target->setUseCache($this->useCache);
190  $target->setMinify($this->minify);
191 
192  }
193 
194 
200  public function addPath($path, $url){
201  $this->includePath[$path] = $url;
202  }
203 
204 
209  public function removePath($path){
210  unset($this->includePath[$path]);
211  }
212 
213 
218  public function getPaths(){
219  return $this->includePath;
220  }
221 
222  public function clearPaths(){
223  $this->includePath = array();
224  }
225 
230  public function import($path){
231  if ( @$this->ignoreScripts[$path] ) return;
232  $this->scripts[$path] = 1;
233  }
234 
235  public function whereis($script){
236  $out = array();
237  foreach ($this->getPaths() as $path=>$url){
238  if ( is_readable($path.DIRECTORY_SEPARATOR.$script) ){
239  $out[] = $path.DIRECTORY_SEPARATOR.$script;
240  }
241  }
242 
243  return $out;
244  }
245 
246  public function which($script){
247  foreach ($this->getPaths() as $path=>$url){
248  if ( is_readable($path.DIRECTORY_SEPARATOR.$script) ){
249  return $path.DIRECTORY_SEPARATOR.$script;
250  }
251  }
252  return null;
253  }
254 
260  public function getURL(){
261  $this->compile();
262  return DATAFACE_SITE_HREF.'?-action=js&--id='.$this->generateCacheKeyForScripts(array_keys($this->scripts));
263  }
264 
268  public function getContents(){
269  $this->compile();
270  return file_get_contents($this->getJavascriptCachePath(array_keys($this->scripts)));
271  }
272 
273  public function getHtml(){
274  $this->compile();
275  $out = array();
276  //print_r($this->dependencies);
277  $clazz = get_class($this);
278  $js = new $clazz;
279  foreach ($this->dependencies as $script=>$path){
280  $js->import($script);
281  $out[] = sprintf('<script src="%s"></script>', htmlspecialchars($js->getURL()));
282  $js->unimport($script);
283  }
284  $out[] = sprintf('<script src="%s"></script>', htmlspecialchars($this->getURL()));
285  return implode("\r\n", $out);
286  }
287 
288  public function unimport($script){
289  unset($this->scripts[$script]);
290  }
291 
296  private function generateCacheKeyForScripts(){
297  //$this->sortScripts();
298  $scripts = array_keys($this->scripts);
299  $base = basename($scripts[0]);
300  $base = substr($base, 0, 10);
301  return $base.'-'.md5(implode(PATH_SEPARATOR, $scripts));
302  }
303 
307  private function writeJavascript($contents){
308  $path = $this->getJavascriptCachePath();
309  return file_put_contents($path, $contents, LOCK_EX);
310  }
311 
315  private function getJavascriptCachePath(){
316  return DATAFACE_SITE_PATH.'/templates_c/'.$this->generateCacheKeyForScripts().'.js';
317  }
318 
319 
323  private function getManifestPath(){
324  return DATAFACE_SITE_PATH.'/templates_c/'.$this->generateCacheKeyForScripts().'.manifest.js';
325  }
326 
327 
335  private function getManifestData(){
336  $path = $this->getManifestPath();
337  if ( is_readable($path) ){
338  return json_decode(file_get_contents($path), true);
339  } else {
340  return array();
341  }
342  }
343 
347  private function prepareManifest(){
348 
349  return array(
350  'included'=> $this->included,
351  'dependencies' => $this->dependencies,
352  'dependencyContents' => $this->dependencyContents,
353  'cssIncludes' => $this->css->getIncluded()
354  );
355  }
356 
357 
370  private function writeManifest(){
371  $data = $this->prepareManifest();
372  $path = $this->getManifestPath();
373  return file_put_contents($path, json_encode($data), LOCK_EX);
374  }
375 
376 
383  private function isCacheDirty(){
384  $jspath = $this->getJavascriptCachePath();
385  $manifest = $this->getManifestData();
386 
387  if ( !file_exists($jspath) ) return true;
388  if ( !$manifest ) return true;
389  if ( !$manifest['dependencyContents'] ) return true;
390  $mtime = filemtime($jspath);
391 
392  $deps = $manifest['dependencyContents'];
393  foreach ($deps as $script=>$file){
394  $t = @filemtime($file);
395  if ( !$t or $t > $mtime ){
396  return true;
397  }
398  }
399 
400 
401  return false;
402 
403 
404  }
405 
406 
407 
408 
409 
410  public function compile($clean=false){
411  if ( !$this->useCache ) $clean = true;
412  $scripts = array_keys($this->scripts);
413 
414 
415  if ( $clean or $this->isCacheDirty() ){
416  $this->included = array();
417  $this->dependencies = array();
418  $this->dependencyContents = array();
419  $this->cssIncludes = array();
420 
421  $this->mergeCSSPaths();
422  $contents = $this->_compile($scripts);
423 
424 
425 
426  $css = $this->css;
427  if ( $css->getStylesheets() ){
428 
429 
430  $contents = sprintf("\r\n".'(function(){
431  var headtg = document.getElementsByTagName("head")[0];
432  if ( !headtg ) return;
433  var linktg = document.createElement("link");
434  linktg.type = "text/css";
435  linktg.rel = "stylesheet";
436  linktg.href="%s";
437  linktg.title="Styles";
438  headtg.appendChild(linktg);
439  })();', $css->getURL())
440 
441  .$contents;
442 
443  }
444 
445 
446 
447  $contents = "if ( typeof(window.console)=='undefined' ){window.console = {log: function(str){}};}if ( typeof(window.__xatajax_included__) != 'object' ){window.__xatajax_included__={};};"
448  .$contents.'
449  if ( typeof(XataJax) != "undefined" ) XataJax.ready();
450  ';
451 
452  if ( $this->minify ) $contents = JSMin::minify($contents);
453  $res = file_put_contents($this->getJavascriptCachePath(), $contents, LOCK_EX);
454  if ( $res === false ){
455  throw new Exception("JavascriptTool failed cache the request's javascript file. Please check that your application has a templates_c directory and that it is writable.");
456 
457  }
458  //$res = file_put_contents($this->getManifestPath(), json_encode(array_merge($this->included, $css->getIncluded())), LOCK_EX);
459  $res = $this->writeManifest();
460  if ( $res === false ){
461  throw new Exception("JavascriptTool failed cache the request's manifest file. Please check that your application has a templates_c directory and that it is writable.");
462 
463  }
464  }
465 
466 
467  }
468 
469 
470 
471 
472  private function processDependency($script){
473  if ( @$this->ignoreScripts[$script] ) return;
474  $scriptPath = $this->which($script);
475  if ( !$scriptPath ) throw new Exception(sprintf('Dependency "%s" could not be found in include path.', $script));
476  $this->dependencies[$script] = $scriptPath;
477 
478  $clazz = get_class($this);
479  $js = new $clazz;
480  $js->clearPaths();
481  foreach ($this->getPaths() as $k=>$v){
482  $js->addPath($k,$v);
483  }
484  //echo "PRocessing dependency $script";
485  $js->import($script);
486  $js->compile();
487  $data = $js->getManifestData();
488 
489  $contents = $data['dependencyContents'];
490  foreach ($contents as $k=>$v){
491  $this->dependencyContents[$k] = $v;
492  }
493  foreach ( $js->getScripts() as $k=>$v){
494  $this->dependencies[$k] = $v;
495  }
496  foreach ($data['dependencies'] as $k=>$v){
497  $this->dependencies[$k] = $v;
498  }
499 
500  }
501 
502  protected function decorateContents($contents, $script){
503  return $contents;
504  }
505 
506 
521  protected function _compile($scripts, $passthru=false, $onceOnly=true){
522  //$included = array();
523  $out=array();
524  if ( !is_array($scripts) ) $scripts = array($scripts);
525  $included =& $this->dependencyContents;
526 
527  // Go through each script
528  foreach ($scripts as $script){
529  $contents = null;
530  if ( $onceOnly and isset($included[$script]) or @$this->ignoreScripts[$script] ) continue;
531 
532  foreach ($this->includePath as $path=>$url){
533  $filepath = $path.DIRECTORY_SEPARATOR.$script;
534  //echo "\nChecking $filepath\n";
535  if ( is_readable($filepath) ){
536  $contents = file_get_contents($filepath);
537  if ( !$passthru ){
538  $contents = $this->decorateContents($contents, $script);
539 
540  if ( preg_match_all('#//load <(.*?)>#', $contents, $matches, PREG_SET_ORDER) ){
541 
542  foreach ($matches as $match){
543 
544  $this->processDependency($match[1]);
545  }
546  }
547  }
548  $included[$script] = $filepath;
549  $this->included[$script] = $filepath;
550  break;
551  }
552  }
553 
554  if ( !isset($contents) ) {
555  throw new Exception(sprintf("Could not find script %s", $script));
556  }
557 
558  if ( !$passthru ){
559  try {
560  $contents = preg_replace_callback('#//(require|include|require-css) <(.*)>#', array($this, '_importCallback'), $contents);
561  $contents = preg_replace_callback('#@@\((.*?)\)#', array($this, '_includeStringCallback'), $contents);
562  } catch (Exception $ex){
563  //die('here');
564  error_log($ex->getMessage());
565  echo $ex->getMessage();
566  throw new Exception(
567  'Server-side Javascript directive failed in script "'.$script.'"',
568  0
569 
570  );
571 
572  }
573  $contents = "\r\n//START ".$script."\r\n"
574  .sprintf("if ( typeof(window.__xatajax_included__['%s']) == 'undefined'){window.__xatajax_included__['%s'] = true;\r\n", addslashes($script), addslashes($script))
575  .$contents."\r\n//END ".$script."\r\n"
576  ."\r\n}";
577  }
578 
579  $out[] = $contents;
580 
581 
582  }
583  return implode("\r\n", $out);
584  }
585 
586  public function _includeStringCallback($matches){
587  return json_encode($this->_compile($matches[1], true, false));
588  }
589 
590  public function _importCallback($matches){
591  switch ($matches[1]){
592  case 'require':
593  if ( isset($this->dependencyContents[$matches[2]]) ){
594  return '';
595  }
596  return "\r\n".$this->_compile($matches[2]);
597  break;
598  case 'include':
599  return "\r\n".$this->_compile($matches[2]);
600  case 'require-css':
601  $css = $this->css;
602  $css->import($matches[2]);
603 
604  return '';
605  default:
606  throw new Exception("Handling import callback but no valid directive found");
607  }
608  }
609 
610  public function clearCache(){
611  $files = glob(DATAFACE_SITE_PATH.'/templates_c/*.js');
612  foreach($files as $f){
613  unlink($f);
614  }
615  $files = glob(DATAFACE_SITE_PATH.'/templates_c/*.manifest.js');
616  foreach($files as $f){
617  unlink($f);
618  }
619  }
620 
621 
622 }
623 
624 
676 class JSMin {
677  const ORD_LF = 10;
678  const ORD_SPACE = 32;
679  const ACTION_KEEP_A = 1;
680  const ACTION_DELETE_A = 2;
681  const ACTION_DELETE_A_B = 3;
682 
683  protected $a = "\n";
684  protected $b = '';
685  protected $input = '';
686  protected $inputIndex = 0;
687  protected $inputLength = 0;
688  protected $lookAhead = null;
689  protected $output = '';
690 
697  public static function minify($js)
698  {
699  $jsmin = new JSMin($js);
700  return $jsmin->min();
701  }
702 
706  public function __construct($input)
707  {
708  $this->input = str_replace("\r\n", "\n", $input);
709  $this->inputLength = strlen($this->input);
710  }
711 
715  public function min()
716  {
717  if ($this->output !== '') { // min already run
718  return $this->output;
719  }
720  $this->action(self::ACTION_DELETE_A_B);
721 
722  while ($this->a !== null) {
723  // determine next command
724  $command = self::ACTION_KEEP_A; // default
725  if ($this->a === ' ') {
726  if (! $this->isAlphaNum($this->b)) {
727  $command = self::ACTION_DELETE_A;
728  }
729  } elseif ($this->a === "\n") {
730  if ($this->b === ' ') {
731  $command = self::ACTION_DELETE_A_B;
732  } elseif (false === strpos('{[(+-', $this->b)
733  && ! $this->isAlphaNum($this->b)) {
734  $command = self::ACTION_DELETE_A;
735  }
736  } elseif (! $this->isAlphaNum($this->a)) {
737  if ($this->b === ' '
738  || ($this->b === "\n"
739  && (false === strpos('}])+-"\'', $this->a)))) {
740  $command = self::ACTION_DELETE_A_B;
741  }
742  }
743  $this->action($command);
744  }
745  $this->output = trim($this->output);
746  return $this->output;
747  }
748 
754  protected function action($command)
755  {
756  switch ($command) {
757  case self::ACTION_KEEP_A:
758  $this->output .= $this->a;
759  // fallthrough
760  case self::ACTION_DELETE_A:
761  $this->a = $this->b;
762  if ($this->a === "'" || $this->a === '"') { // string literal
763  $str = $this->a; // in case needed for exception
764  while (true) {
765  $this->output .= $this->a;
766  $this->a = $this->get();
767  if ($this->a === $this->b) { // end quote
768  break;
769  }
770  if (ord($this->a) <= self::ORD_LF) {
772  'Unterminated String: ' . var_export($str, true));
773  }
774  $str .= $this->a;
775  if ($this->a === '\\') {
776  $this->output .= $this->a;
777  $this->a = $this->get();
778  $str .= $this->a;
779  }
780  }
781  }
782  // fallthrough
783  case self::ACTION_DELETE_A_B:
784  $this->b = $this->next();
785  if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal
786  $this->output .= $this->a . $this->b;
787  $pattern = '/'; // in case needed for exception
788  while (true) {
789  $this->a = $this->get();
790  $pattern .= $this->a;
791  if ($this->a === '/') { // end pattern
792  break; // while (true)
793  } elseif ($this->a === '\\') {
794  $this->output .= $this->a;
795  $this->a = $this->get();
796  $pattern .= $this->a;
797  } elseif (ord($this->a) <= self::ORD_LF) {
799  'Unterminated RegExp: '. var_export($pattern, true));
800  }
801  $this->output .= $this->a;
802  }
803  $this->b = $this->next();
804  }
805  // end case ACTION_DELETE_A_B
806  }
807  }
808 
809  protected function isRegexpLiteral()
810  {
811  if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing
812  return true;
813  }
814  if (' ' === $this->a) {
815  $length = strlen($this->output);
816  if ($length < 2) { // weird edge case
817  return true;
818  }
819  // you can't divide a keyword
820  if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
821  if ($this->output === $m[0]) { // odd but could happen
822  return true;
823  }
824  // make sure it's a keyword, not end of an identifier
825  $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
826  if (! $this->isAlphaNum($charBeforeKeyword)) {
827  return true;
828  }
829  }
830  }
831  return false;
832  }
833 
837  protected function get()
838  {
839  $c = $this->lookAhead;
840  $this->lookAhead = null;
841  if ($c === null) {
842  if ($this->inputIndex < $this->inputLength) {
843  $c = $this->input[$this->inputIndex];
844  $this->inputIndex += 1;
845  } else {
846  return null;
847  }
848  }
849  if ($c === "\r" || $c === "\n") {
850  return "\n";
851  }
852  if (ord($c) < self::ORD_SPACE) { // control char
853  return ' ';
854  }
855  return $c;
856  }
857 
861  protected function peek()
862  {
863  $this->lookAhead = $this->get();
864  return $this->lookAhead;
865  }
866 
870  protected function isAlphaNum($c)
871  {
872  return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
873  }
874 
875  protected function singleLineComment()
876  {
877  $comment = '';
878  while (true) {
879  $get = $this->get();
880  $comment .= $get;
881  if (ord($get) <= self::ORD_LF) { // EOL reached
882  // if IE conditional comment
883  if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
884  return "/{$comment}";
885  }
886  return $get;
887  }
888  }
889  }
890 
891  protected function multipleLineComment()
892  {
893  $this->get();
894  $comment = '';
895  while (true) {
896  $get = $this->get();
897  if ($get === '*') {
898  if ($this->peek() === '/') { // end of comment reached
899  $this->get();
900  // if comment preserved by YUI Compressor
901  if (0 === strpos($comment, '!')) {
902  return "\n/*" . substr($comment, 1) . "*/\n";
903  }
904  // if IE conditional comment
905  if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
906  return "/*{$comment}*/";
907  }
908  return ' ';
909  }
910  } elseif ($get === null) {
911  throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true));
912  }
913  $comment .= $get;
914  }
915  }
916 
921  protected function next()
922  {
923  $get = $this->get();
924  if ($get !== '/') {
925  return $get;
926  }
927  switch ($this->peek()) {
928  case '/': return $this->singleLineComment();
929  case '*': return $this->multipleLineComment();
930  default: return $get;
931  }
932  }
933 }
934 
935 class JSMin_UnterminatedStringException extends Exception {}
936 class JSMin_UnterminatedCommentException extends Exception {}
937 class JSMin_UnterminatedRegExpException extends Exception {}
938 
939 //}