source: trunk/include/template.class.php @ 10503

Last change on this file since 10503 was 10503, checked in by rvelices, 13 years ago

bug:2268 combine_script - sometimes precedent scripts were not being loaded before ...
in admin, the accordion is loaded in the header (admin pages have the same header combined script, but usually different bottom scripts)

  • Property svn:eol-style set to LF
File size: 40.0 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2011 Piwigo Team                  http://piwigo.org |
6// | Copyright(C) 2003-2008 PhpWebGallery Team    http://phpwebgallery.net |
7// | Copyright(C) 2002-2003 Pierrick LE GALL   http://le-gall.net/pierrick |
8// +-----------------------------------------------------------------------+
9// | This program is free software; you can redistribute it and/or modify  |
10// | it under the terms of the GNU General Public License as published by  |
11// | the Free Software Foundation                                          |
12// |                                                                       |
13// | This program is distributed in the hope that it will be useful, but   |
14// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
15// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
16// | General Public License for more details.                              |
17// |                                                                       |
18// | You should have received a copy of the GNU General Public License     |
19// | along with this program; if not, write to the Free Software           |
20// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
21// | USA.                                                                  |
22// +-----------------------------------------------------------------------+
23
24
25require_once(PHPWG_ROOT_PATH.'include/smarty/libs/Smarty.class.php');
26
27
28class Template {
29
30  var $smarty;
31
32  var $output = '';
33
34  // Hash of filenames for each template handle.
35  var $files = array();
36
37  // Template extents filenames for each template handle.
38  var $extents = array();
39
40  // Templates prefilter from external sources (plugins)
41  var $external_filters = array();
42
43  // used by html_head smarty block to add content before </head>
44  var $html_head_elements = array();
45
46  const COMBINED_SCRIPTS_TAG = '<!-- COMBINED_SCRIPTS -->';
47  var $scriptLoader;
48
49  const COMBINED_CSS_TAG = '<!-- COMBINED_CSS -->';
50  var $css_by_priority = array();
51
52  function Template($root = ".", $theme= "", $path = "template")
53  {
54    global $conf, $lang_info;
55
56    $this->scriptLoader = new ScriptLoader;
57    $this->smarty = new Smarty;
58    $this->smarty->debugging = $conf['debug_template'];
59    $this->smarty->compile_check = $conf['template_compile_check'];
60    $this->smarty->force_compile = $conf['template_force_compile'];
61
62    if (!isset($conf['local_data_dir_checked']))
63    {
64      mkgetdir($conf['local_data_dir'], MKGETDIR_DEFAULT&~MKGETDIR_DIE_ON_ERROR);
65      if (!is_writable($conf['local_data_dir']))
66      {
67        load_language('admin.lang');
68        fatal_error(
69          sprintf(
70            l10n('Give write access (chmod 777) to "%s" directory at the root of your Piwigo installation'),
71            basename($conf['local_data_dir'])
72            ),
73          l10n('an error happened'),
74          false // show trace
75          );
76      }
77      if (function_exists('pwg_query')) {
78        conf_update_param('local_data_dir_checked', 'true');
79      }
80    }
81
82    if (!isset($conf['combined_dir_checked']))
83    {
84      mkgetdir(PWG_COMBINED_DIR, MKGETDIR_DEFAULT&~MKGETDIR_DIE_ON_ERROR);
85      if (!is_writable(PWG_COMBINED_DIR))
86      {
87        load_language('admin.lang');
88        fatal_error(
89          sprintf(
90            l10n('Give write access (chmod 777) to "%s" directory at the root of your Piwigo installation'),
91            PWG_COMBINED_DIR
92            ),
93          l10n('an error happened'),
94          false // show trace
95          );
96      }
97      if (function_exists('pwg_query')) {
98        conf_update_param('combined_dir_checked', 'true');
99      }
100    }
101
102
103    $compile_dir = $conf['local_data_dir'].'/templates_c';
104    mkgetdir( $compile_dir );
105
106    $this->smarty->compile_dir = $compile_dir;
107
108    $this->smarty->assign_by_ref( 'pwg', new PwgTemplateAdapter() );
109    $this->smarty->register_modifier( 'translate', array('Template', 'mod_translate') );
110    $this->smarty->register_modifier( 'explode', array('Template', 'mod_explode') );
111    $this->smarty->register_modifier( 'get_extent', array(&$this, 'get_extent') );
112    $this->smarty->register_block('html_head', array(&$this, 'block_html_head') );
113    $this->smarty->register_function('combine_script', array(&$this, 'func_combine_script') );
114    $this->smarty->register_function('get_combined_scripts', array(&$this, 'func_get_combined_scripts') );
115    $this->smarty->register_function('combine_css', array(&$this, 'func_combine_css') );
116    $this->smarty->register_compiler_function('get_combined_css', array(&$this, 'func_get_combined_css') );
117    $this->smarty->register_block('footer_script', array(&$this, 'block_footer_script') );
118    $this->smarty->register_function('known_script', array(&$this, 'func_known_script') );
119    $this->smarty->register_prefilter( array('Template', 'prefilter_white_space') );
120    if ( $conf['compiled_template_cache_language'] )
121    {
122      $this->smarty->register_prefilter( array('Template', 'prefilter_language') );
123    }
124
125    $this->smarty->template_dir = array();
126    if ( !empty($theme) )
127    {
128      $this->set_theme($root, $theme, $path);
129      $this->set_prefilter( 'header', array('Template', 'prefilter_local_css') );
130    }
131    else
132      $this->set_template_dir($root);
133
134    $this->smarty->assign('lang_info', $lang_info);
135
136    if (!defined('IN_ADMIN') and isset($conf['extents_for_templates']))
137    {
138      $tpl_extents = unserialize($conf['extents_for_templates']);
139      $this->set_extents($tpl_extents, './template-extension/', true, $theme);
140    }
141  }
142
143  /**
144   * Load theme's parameters.
145   */
146  function set_theme($root, $theme, $path, $load_css=true, $load_local_head=true)
147  {
148    $this->set_template_dir($root.'/'.$theme.'/'.$path);
149
150    $themeconf = $this->load_themeconf($root.'/'.$theme);
151
152    if (isset($themeconf['parent']) and $themeconf['parent'] != $theme)
153    {
154      $this->set_theme(
155        $root,
156        $themeconf['parent'],
157        $path,
158        isset($themeconf['load_parent_css']) ? $themeconf['load_parent_css'] : $load_css,
159        isset($themeconf['load_parent_local_head']) ? $themeconf['load_parent_local_head'] : $load_local_head
160      );
161    }
162
163    $tpl_var = array(
164      'id' => $theme,
165      'load_css' => $load_css,
166    );
167    if (!empty($themeconf['local_head']) and $load_local_head)
168    {
169      $tpl_var['local_head'] = realpath($root.'/'.$theme.'/'.$themeconf['local_head'] );
170    }
171    $themeconf['id'] = $theme;
172    $this->smarty->append('themes', $tpl_var);
173    $this->smarty->append('themeconf', $themeconf, true);
174  }
175
176  /**
177   * Add template directory for this Template object.
178   * Set compile id if not exists.
179   */
180  function set_template_dir($dir)
181  {
182    $this->smarty->template_dir[] = $dir;
183
184    if (!isset($this->smarty->compile_id))
185    {
186      $real_dir = realpath($dir);
187      $compile_id = crc32( $real_dir===false ? $dir : $real_dir);
188      $this->smarty->compile_id = base_convert($compile_id, 10, 36 );
189    }
190  }
191
192  /**
193   * Gets the template root directory for this Template object.
194   */
195  function get_template_dir()
196  {
197    return $this->smarty->template_dir;
198  }
199
200  /**
201   * Deletes all compiled templates.
202   */
203  function delete_compiled_templates()
204  {
205      $save_compile_id = $this->smarty->compile_id;
206      $this->smarty->compile_id = null;
207      $this->smarty->clear_compiled_tpl();
208      $this->smarty->compile_id = $save_compile_id;
209      file_put_contents($this->smarty->compile_dir.'/index.htm', 'Not allowed!');
210  }
211
212  function get_themeconf($val)
213  {
214    $tc = $this->smarty->get_template_vars('themeconf');
215    return isset($tc[$val]) ? $tc[$val] : '';
216  }
217
218  /**
219   * Sets the template filename for handle.
220   */
221  function set_filename($handle, $filename)
222  {
223    return $this->set_filenames( array($handle=>$filename) );
224  }
225
226  /**
227   * Sets the template filenames for handles. $filename_array should be a
228   * hash of handle => filename pairs.
229   */
230  function set_filenames($filename_array)
231  {
232    if (!is_array($filename_array))
233    {
234      return false;
235    }
236    reset($filename_array);
237    while(list($handle, $filename) = each($filename_array))
238    {
239      if (is_null($filename))
240      {
241        unset($this->files[$handle]);
242      }
243      else
244      {
245        $this->files[$handle] = $this->get_extent($filename, $handle);
246      }
247    }
248    return true;
249  }
250
251  /**
252   * Sets template extention filename for handles.
253   */
254  function set_extent($filename, $param, $dir='', $overwrite=true, $theme='N/A')
255  {
256    return $this->set_extents(array($filename => $param), $dir, $overwrite);
257  }
258
259  /**
260   * Sets template extentions filenames for handles.
261   * $filename_array should be an hash of filename => array( handle, param) or filename => handle
262   */
263  function set_extents($filename_array, $dir='', $overwrite=true, $theme='N/A')
264  {
265    if (!is_array($filename_array))
266    {
267      return false;
268    }
269    foreach ($filename_array as $filename => $value)
270    {
271      if (is_array($value))
272      {
273        $handle = $value[0];
274        $param = $value[1];
275        $thm = $value[2];
276      }
277      elseif (is_string($value))
278      {
279        $handle = $value;
280        $param = 'N/A';
281        $thm = 'N/A';
282      }
283      else
284      {
285        return false;
286      }
287
288      if ((stripos(implode('',array_keys($_GET)), '/'.$param) !== false or $param == 'N/A')
289        and ($thm == $theme or $thm == 'N/A')
290        and (!isset($this->extents[$handle]) or $overwrite)
291        and file_exists($dir . $filename))
292      {
293        $this->extents[$handle] = realpath($dir . $filename);
294      }
295    }
296    return true;
297  }
298
299  /** return template extension if exists  */
300  function get_extent($filename='', $handle='')
301  {
302    if (isset($this->extents[$handle]))
303    {
304      $filename = $this->extents[$handle];
305    }
306    return $filename;
307  }
308
309  /** see smarty assign http://www.smarty.net/manual/en/api.assign.php */
310  function assign($tpl_var, $value = null)
311  {
312    $this->smarty->assign( $tpl_var, $value );
313  }
314
315  /**
316   * Inserts the uncompiled code for $handle as the value of $varname in the
317   * root-level. This can be used to effectively include a template in the
318   * middle of another template.
319   * This is equivalent to assign($varname, $this->parse($handle, true))
320   */
321  function assign_var_from_handle($varname, $handle)
322  {
323    $this->assign($varname, $this->parse($handle, true));
324    return true;
325  }
326
327  /** see smarty append http://www.smarty.net/manual/en/api.append.php */
328  function append($tpl_var, $value=null, $merge=false)
329  {
330    $this->smarty->append( $tpl_var, $value, $merge );
331  }
332
333  /**
334   * Root-level variable concatenation. Appends a  string to an existing
335   * variable assignment with the same name.
336   */
337  function concat($tpl_var, $value)
338  {
339    $old_val = & $this->smarty->get_template_vars($tpl_var);
340    if ( isset($old_val) )
341    {
342      $old_val .= $value;
343    }
344    else
345    {
346      $this->assign($tpl_var, $value);
347    }
348  }
349
350  /** see smarty append http://www.smarty.net/manual/en/api.clear_assign.php */
351  function clear_assign($tpl_var)
352  {
353    $this->smarty->clear_assign( $tpl_var );
354  }
355
356  /** see smarty get_template_vars http://www.smarty.net/manual/en/api.get_template_vars.php */
357  function &get_template_vars($name=null)
358  {
359    return $this->smarty->get_template_vars( $name );
360  }
361
362
363  /**
364   * Load the file for the handle, eventually compile the file and run the compiled
365   * code. This will add the output to the results or return the result if $return
366   * is true.
367   */
368  function parse($handle, $return=false)
369  {
370    if ( !isset($this->files[$handle]) )
371    {
372      fatal_error("Template->parse(): Couldn't load template file for handle $handle");
373    }
374
375    $this->smarty->assign( 'ROOT_URL', get_root_url() );
376
377    $save_compile_id = $this->smarty->compile_id;
378    $this->load_external_filters($handle);
379
380    global $conf, $lang_info;
381    if ( $conf['compiled_template_cache_language'] and isset($lang_info['code']) )
382    {
383      $this->smarty->compile_id .= '.'.$lang_info['code'];
384    }
385
386    $v = $this->smarty->fetch($this->files[$handle], null, null, false);
387
388    $this->smarty->compile_id = $save_compile_id;
389    $this->unload_external_filters($handle);
390
391    if ($return)
392    {
393      return $v;
394    }
395    $this->output .= $v;
396  }
397
398  /**
399   * Load the file for the handle, eventually compile the file and run the compiled
400   * code. This will print out the results of executing the template.
401   */
402  function pparse($handle)
403  {
404    $this->parse($handle, false);
405    $this->flush();
406  }
407
408  function flush()
409  {
410    if (!$this->scriptLoader->did_head())
411    {
412      $pos = strpos( $this->output, self::COMBINED_SCRIPTS_TAG );
413      if ($pos !== false)
414      {
415          $scripts = $this->scriptLoader->get_head_scripts();
416          $content = array();
417          foreach ($scripts as $script)
418          {
419              $content[]=
420                  '<script type="text/javascript" src="'
421                  . self::make_script_src($script)
422                  .'"></script>';
423          }
424
425          $this->output = substr_replace( $this->output, "\n".implode( "\n", $content ), $pos, strlen(self::COMBINED_SCRIPTS_TAG) );
426      } //else maybe error or warning ?
427    }
428
429    if(!empty($this->css_by_priority))
430    {
431      ksort($this->css_by_priority);
432
433      global $conf;
434      $css = array();
435      if ($conf['template_combine_files'])
436      {
437        $combiner = new FileCombiner('css');
438        foreach ($this->css_by_priority as $files)
439        {
440          foreach ($files as $file_ver)
441            $combiner->add( $file_ver[0], $file_ver[1] );
442        }
443        if ( $combiner->combine( $out_file, $out_version) )
444          $css[] = array($out_file, $out_version);
445      }
446      else
447      {
448        foreach ($this->css_by_priority as $files)
449          $css = array_merge($css, $files);
450      }
451
452      $content = array();
453      foreach( $css as $file_ver )
454      {
455        $href = get_root_url() . $file_ver[0];
456        if ($file_ver[1] !== false)
457          $href .= '?v' . ($file_ver[1] ? $file_ver[1] : PHPWG_VERSION);
458        // trigger the event for eventual use of a cdn
459        $href = trigger_event('combined_css', $href, $file_ver[0], $file_ver[1]);
460        $content[] = '<link rel="stylesheet" type="text/css" href="'.$href.'">';
461      }
462      $this->output = str_replace(self::COMBINED_CSS_TAG,
463          implode( "\n", $content ),
464          $this->output );
465                        $this->css_by_priority = array();
466    }
467
468    if ( count($this->html_head_elements) )
469    {
470      $search = "\n</head>";
471      $pos = strpos( $this->output, $search );
472      if ($pos !== false)
473      {
474        $this->output = substr_replace( $this->output, "\n".implode( "\n", $this->html_head_elements ), $pos, 0 );
475      } //else maybe error or warning ?
476      $this->html_head_elements = array();
477    }
478
479    echo $this->output;
480    $this->output='';
481  }
482
483  /** flushes the output */
484  function p()
485  {
486    $this->flush();
487
488    if ($this->smarty->debugging)
489    {
490      global $t2;
491      $this->smarty->assign(
492        array(
493        'AAAA_DEBUG_TOTAL_TIME__' => get_elapsed_time($t2, get_moment())
494        )
495        );
496      require_once(SMARTY_CORE_DIR . 'core.display_debug_console.php');
497      echo smarty_core_display_debug_console(null, $this->smarty);
498    }
499  }
500
501  /**
502   * translate variable modifier - translates a text to the currently loaded
503   * language
504   */
505  static function mod_translate($text)
506  {
507    return l10n($text);
508  }
509
510  /**
511   * explode variable modifier - similar to php explode
512   * 'Yes;No'|@explode:';' -> array('Yes', 'No')
513   */
514  static function mod_explode($text, $delimiter=',')
515  {
516    return explode($delimiter, $text);
517  }
518
519  /**
520   * This smarty "html_head" block allows to add content just before
521   * </head> element in the output after the head has been parsed. This is
522   * handy in order to respect strict standards when <style> and <link>
523   * html elements must appear in the <head> element
524   */
525  function block_html_head($params, $content, &$smarty, &$repeat)
526  {
527    $content = trim($content);
528    if ( !empty($content) )
529    { // second call
530      $this->html_head_elements[] = $content;
531    }
532  }
533
534 /**
535   * This smarty "known_script" functions allows to insert well known java scripts
536   * such as prototype, jquery, etc... only once. Examples:
537   * {known_script id="jquery" src="{$ROOT_URL}template-common/lib/jquery.packed.js"}
538   */
539  function func_known_script($params, &$smarty )
540  {
541    if (!isset($params['id']))
542    {
543        $smarty->trigger_error("known_script: missing 'id' parameter");
544        return;
545    }
546    $id = $params['id'];
547    trigger_error("known_script is deprecated $id ".@$params['src'], E_USER_WARNING);
548    if ('jquery'==$id)
549    {
550      $this->scriptLoader->add($id, 0, array(), null);
551      return;
552    }
553    if (! isset( $this->known_scripts[$id] ) )
554    {
555      if (!isset($params['src']))
556      {
557          $smarty->trigger_error("known_script: missing 'src' parameter");
558          return;
559      }
560      $this->known_scripts[$id] = $params['src'];
561      $content = '<script type="text/javascript" src="'.$params['src'].'"></script>';
562      if (isset($params['now']) and $params['now'] and empty($this->output) )
563      {
564        return $content;
565      }
566      $repeat = false;
567      $this->block_html_head(null, $content, $smarty, $repeat);
568    }
569  }
570
571  /**
572    * combine_script smarty function allows inclusion of a javascript file in the current page.
573    * The engine will combine several js files into a single one in order to reduce the number of
574    * required http requests.
575    * param id - required
576    * param path - required - the path to js file RELATIVE to piwigo root dir
577    * param load - optional - header|footer|async, default header
578    * param require - optional - comma separated list of script ids required to be loaded and executed
579        before this one
580    * param version - optional - plugins could use this and change it in order to force a
581        browser refresh
582    */
583  function func_combine_script($params, &$smarty)
584  {
585    if (!isset($params['id']))
586    {
587      $smarty->trigger_error("combine_script: missing 'id' parameter", E_USER_ERROR);
588    }
589    $load = 0;
590    if (isset($params['load']))
591    {
592      switch ($params['load'])
593      {
594        case 'header': break;
595        case 'footer': $load=1; break;
596        case 'async': $load=2; break;
597        default: $smarty->trigger_error("combine_script: invalid 'load' parameter", E_USER_ERROR);
598      }
599    }
600    $this->scriptLoader->add( $params['id'], $load,
601      empty($params['require']) ? array() : explode( ',', $params['require'] ),
602      @$params['path'],
603      isset($params['version']) ? $params['version'] : 0 );
604  }
605
606
607  function func_get_combined_scripts($params, &$smarty)
608  {
609    if (!isset($params['load']))
610    {
611      $smarty->trigger_error("get_combined_scripts: missing 'load' parameter", E_USER_ERROR);
612    }
613    $load = $params['load']=='header' ? 0 : 1;
614    $content = array();
615
616    if ($load==0)
617    {
618      return self::COMBINED_SCRIPTS_TAG;
619    }
620    else
621    {
622      $scripts = $this->scriptLoader->get_footer_scripts();
623      foreach ($scripts[0] as $script)
624      {
625        $content[]=
626          '<script type="text/javascript" src="'
627          . self::make_script_src($script)
628          .'"></script>';
629      }
630      if (count($this->scriptLoader->inline_scripts))
631      {
632        $content[]= '<script type="text/javascript">//<![CDATA[
633';
634        $content = array_merge($content, $this->scriptLoader->inline_scripts);
635        $content[]= '//]]></script>';
636      }
637
638      if (count($scripts[1]))
639      {
640        $content[]= '<script type="text/javascript">';
641        $content[]= '(function() {
642var s,after = document.getElementsByTagName(\'script\')[document.getElementsByTagName(\'script\').length-1];';
643        foreach ($scripts[1] as $id => $script)
644        {
645          $content[]=
646            's=document.createElement(\'script\'); s.type=\'text/javascript\'; s.async=true; s.src=\''
647            . self::make_script_src($script)
648            .'\';';
649          $content[]= 'after = after.parentNode.insertBefore(s, after);';
650        }
651        $content[]= '})();';
652        $content[]= '</script>';
653      }
654    }
655    return implode("\n", $content);
656  }
657
658
659  private static function make_script_src( $script )
660  {
661    $ret = '';
662    if ( $script->is_remote() )
663      $ret = $script->path;
664    else
665    {
666      $ret = get_root_url().$script->path;
667      if ($script->version!==false)
668      {
669        $ret.= '?v'. ($script->version ? $script->version : PHPWG_VERSION);
670      }
671    }
672    // trigger the event for eventual use of a cdn
673    $ret = trigger_event('combined_script', $ret, $script);
674    return $ret;
675  }
676
677  function block_footer_script($params, $content, &$smarty, &$repeat)
678  {
679    $content = trim($content);
680    if ( !empty($content) )
681    { // second call
682      $this->scriptLoader->add_inline(
683        $content,
684        empty($params['require']) ? array() : explode(',', $params['require'])
685      );
686    }
687  }
688
689  /**
690    * combine_css smarty function allows inclusion of a css stylesheet file in the current page.
691    * The engine will combine several css files into a single one in order to reduce the number of
692    * required http requests.
693    * param path - required - the path to css file RELATIVE to piwigo root dir
694    * param version - optional - plugins could use this and change it in order to force a
695        browser refresh
696    */
697  function func_combine_css($params, &$smarty)
698  {
699    !empty($params['path']) || fatal_error('combine_css missing path');
700    $order = (int)@$params['order'];
701    $version = isset($params['version']) ? $params['version'] : 0;
702    $this->css_by_priority[$order][] = array( $params['path'], $version);
703  }
704
705  function func_get_combined_css($params, &$smarty)
706  {
707    return 'echo '.var_export(self::COMBINED_CSS_TAG,true);
708  }
709
710
711 /**
712   * This function allows to declare a Smarty prefilter from a plugin, thus allowing
713   * it to modify template source before compilation and without changing core files
714   * They will be processed by weight ascending.
715   * http://www.smarty.net/manual/en/advanced.features.prefilters.php
716   */
717  function set_prefilter($handle, $callback, $weight=50)
718  {
719    $this->external_filters[$handle][$weight][] = array('prefilter', $callback);
720    ksort($this->external_filters[$handle]);
721  }
722
723  function set_postfilter($handle, $callback, $weight=50)
724  {
725    $this->external_filters[$handle][$weight][] = array('postfilter', $callback);
726    ksort($this->external_filters[$handle]);
727  }
728
729  function set_outputfilter($handle, $callback, $weight=50)
730  {
731    $this->external_filters[$handle][$weight][] = array('outputfilter', $callback);
732    ksort($this->external_filters[$handle]);
733  }
734
735 /**
736   * This function actually triggers the filters on the tpl files.
737   * Called in the parse method.
738   * http://www.smarty.net/manual/en/advanced.features.prefilters.php
739   */
740  function load_external_filters($handle)
741  {
742    if (isset($this->external_filters[$handle]))
743    {
744      $compile_id = '';
745      foreach ($this->external_filters[$handle] as $filters)
746      {
747        foreach ($filters as $filter)
748        {
749          list($type, $callback) = $filter;
750          $compile_id .= $type.( is_array($callback) ? implode('', $callback) : $callback );
751          call_user_func(array($this->smarty, 'register_'.$type), $callback);
752        }
753      }
754      $this->smarty->compile_id .= '.'.base_convert(crc32($compile_id), 10, 36);
755    }
756  }
757
758  function unload_external_filters($handle)
759  {
760    if (isset($this->external_filters[$handle]))
761    {
762      foreach ($this->external_filters[$handle] as $filters)
763      {
764        foreach ($filters as $filter)
765        {
766          list($type, $callback) = $filter;
767          call_user_func(array($this->smarty, 'unregister_'.$type), $callback);
768        }
769      }
770    }
771  }
772
773  static function prefilter_white_space($source, &$smarty)
774  {
775    $ld = $smarty->left_delimiter;
776    $rd = $smarty->right_delimiter;
777    $ldq = preg_quote($ld, '#');
778    $rdq = preg_quote($rd, '#');
779
780    $regex = array();
781    $tags = array('if','foreach','section','footer_script');
782    foreach($tags as $tag)
783    {
784      array_push($regex, "#^[ \t]+($ldq$tag"."[^$ld$rd]*$rdq)\s*$#m");
785      array_push($regex, "#^[ \t]+($ldq/$tag$rdq)\s*$#m");
786    }
787    $tags = array('include','else','combine_script','html_head');
788    foreach($tags as $tag)
789    {
790      array_push($regex, "#^[ \t]+($ldq$tag"."[^$ld$rd]*$rdq)\s*$#m");
791    }
792    $source = preg_replace( $regex, "$1", $source);
793    return $source;
794  }
795
796  /**
797   * Smarty prefilter to allow caching (whenever possible) language strings
798   * from templates.
799   */
800  static function prefilter_language($source, &$smarty)
801  {
802    global $lang;
803    $ldq = preg_quote($smarty->left_delimiter, '~');
804    $rdq = preg_quote($smarty->right_delimiter, '~');
805
806    $regex = "~$ldq *\'([^'$]+)\'\|@translate *$rdq~";
807    $source = preg_replace( $regex.'e', 'isset($lang[\'$1\']) ? $lang[\'$1\'] : \'$0\'', $source);
808
809    $regex = "~$ldq *\'([^'$]+)\'\|@translate\|~";
810    $source = preg_replace( $regex.'e', 'isset($lang[\'$1\']) ? \'{\'.var_export($lang[\'$1\'],true).\'|\' : \'$0\'', $source);
811
812    $regex = "~($ldq *assign +var=.+ +value=)\'([^'$]+)\'\|@translate~e";
813    $source = preg_replace( $regex, 'isset($lang[\'$2\']) ? \'$1\'.var_export($lang[\'$2\'],true) : \'$0\'', $source);
814
815    return $source;
816  }
817
818  static function prefilter_local_css($source, &$smarty)
819  {
820    $css = array();
821    foreach ($smarty->get_template_vars('themes') as $theme)
822    {
823      $f = PWG_LOCAL_DIR.'css/'.$theme['id'].'-rules.css';
824      if (file_exists(PHPWG_ROOT_PATH.$f))
825      {
826        array_push($css, "{combine_css path='$f' order=10}");
827      }
828    }
829    $f = PWG_LOCAL_DIR.'css/rules.css';
830    if (file_exists(PHPWG_ROOT_PATH.$f))
831    {
832      array_push($css, "{combine_css path='$f' order=10}");
833    }
834
835    if (!empty($css))
836    {
837      $source = str_replace("\n{get_combined_css}", "\n".implode( "\n", $css )."\n{get_combined_css}", $source);
838    }
839
840    return $source;
841  }
842
843  function load_themeconf($dir)
844  {
845    global $themeconfs, $conf;
846
847    $dir = realpath($dir);
848    if (!isset($themeconfs[$dir]))
849    {
850      $themeconf = array();
851      include($dir.'/themeconf.inc.php');
852      // Put themeconf in cache
853      $themeconfs[$dir] = $themeconf;
854    }
855    return $themeconfs[$dir];
856  }
857}
858
859
860/**
861 * This class contains basic functions that can be called directly from the
862 * templates in the form $pwg->l10n('edit')
863 */
864class PwgTemplateAdapter
865{
866  function l10n($text)
867  {
868    return l10n($text);
869  }
870
871  function l10n_dec($s, $p, $v)
872  {
873    return l10n_dec($s, $p, $v);
874  }
875
876  function sprintf()
877  {
878    $args = func_get_args();
879    return call_user_func_array('sprintf',  $args );
880  }
881}
882
883
884final class Script
885{
886  public $id;
887  public $load_mode;
888  public $precedents = array();
889  public $path;
890  public $version;
891  public $extra = array();
892
893  function Script($load_mode, $id, $path, $version, $precedents)
894  {
895    $this->id = $id;
896    $this->load_mode = $load_mode;
897    $this->id = $id;
898    $this->set_path($path);
899    $this->version = $version;
900    $this->precedents = $precedents;
901  }
902
903  function set_path($path)
904  {
905    if (!empty($path))
906      $this->path = $path;
907  }
908
909  function is_remote()
910  {
911    return url_is_remote( $this->path );
912  }
913}
914
915
916/** Manage a list of required scripts for a page, by optimizing their loading location (head, bottom, async)
917and later on by combining them in a unique file respecting at the same time dependencies.*/
918class ScriptLoader
919{
920  private $registered_scripts;
921  public $inline_scripts;
922
923  private $did_head;
924  private $head_done_scripts;
925
926  private static $known_paths = array(
927      'core.scripts' => 'themes/default/js/scripts.js',
928      'jquery' => 'themes/default/js/jquery.min.js',
929      'jquery.ui' => 'themes/default/js/ui/minified/jquery.ui.core.min.js',
930      'jquery.effects' => 'themes/default/js/ui/minified/jquery.effects.core.min.js',
931    );
932
933  private static $ui_core_dependencies = array(
934      'jquery.ui.widget' => array('jquery'),
935      'jquery.ui.position' => array('jquery'),
936      'jquery.ui.mouse' => array('jquery', 'jquery.ui', 'jquery.ui.widget'),
937    );
938
939  function __construct()
940  {
941    $this->clear();
942  }
943
944  function clear()
945  {
946    $this->registered_scripts = array();
947    $this->inline_scripts = array();
948    $this->head_done_scripts = array();
949    $this->did_head = false;
950  }
951
952  function get_all()
953  {
954    return $this->registered_scripts;
955  }
956
957  function add_inline($code, $require)
958  {
959    if(!empty($require))
960    {
961      foreach ($require as $id)
962      {
963        if(!isset($this->registered_scripts[$id]))
964          $this->load_known_required_script($id, 1) or fatal_error("inline script not found require $id");
965        $s = $this->registered_scripts[$id];
966        if($s->load_mode==2)
967          $s->load_mode=1; // until now the implementation does not allow executing inline script depending on another async script
968      }
969    }
970    $this->inline_scripts[] = $code;
971  }
972
973  function add($id, $load_mode, $require, $path, $version=0)
974  {
975    if ($this->did_head && $load_mode==0 )
976    {
977      trigger_error("Attempt to add a new script $id but the head has been written", E_USER_WARNING);
978    }
979    if (! isset( $this->registered_scripts[$id] ) )
980    {
981      $script = new Script($load_mode, $id, $path, $version, $require);
982      self::fill_well_known($id, $script);
983      $this->registered_scripts[$id] = $script;
984    }
985    else
986    {
987      $script = & $this->registered_scripts[$id];
988      if (count($require))
989      {
990        $script->precedents = array_unique( array_merge($script->precedents, $require) );
991      }
992      $script->set_path($path);
993      if ($version && version_compare($script->version, $version)<0 )
994        $script->version = $version;
995      if ($load_mode < $script->load_mode)
996        $script->load_mode = $load_mode;
997    }
998    // Load or modify all UI core files
999    if ($id == 'jquery.ui' and $script->path == self::$known_paths['jquery.ui'])
1000    {
1001      foreach (self::$ui_core_dependencies as $script_id => $required_ids)
1002        $this->add($script_id, $load_mode, $required_ids, null, $version);
1003    }
1004
1005    // Try to load undefined required script
1006    foreach ($script->precedents as $script_id)
1007    {
1008      if (! isset( $this->registered_scripts[$script_id] ) )
1009        $this->load_known_required_script($script_id, $load_mode);
1010    }
1011  }
1012
1013  function did_head()
1014  {
1015    return $this->did_head;
1016  }
1017
1018  function get_head_scripts()
1019  {
1020    self::check_load_dep($this->registered_scripts);
1021    foreach( array_keys($this->registered_scripts) as $id )
1022    {
1023      $this->compute_script_topological_order($id);
1024    }
1025
1026    uasort($this->registered_scripts, array('ScriptLoader', 'cmp_by_mode_and_order'));
1027
1028    foreach( $this->registered_scripts as $id => $script)
1029    {
1030      if ($script->load_mode > 0)
1031        break;
1032      if ( !empty($script->path) )
1033        $this->head_done_scripts[$id] = $script;
1034      else
1035        trigger_error("Script $id has an undefined path", E_USER_WARNING);
1036    }
1037    $this->did_head = true;
1038    return self::do_combine($this->head_done_scripts, 0);
1039  }
1040
1041  function get_footer_scripts()
1042  {
1043    $todo = array();
1044    foreach( $this->registered_scripts as $id => $script)
1045    {
1046      if (!isset($this->head_done_scripts[$id]))
1047      {
1048        $todo[$id] = $script;
1049      }
1050    }
1051
1052    foreach( array_keys($todo) as $id )
1053    {
1054      $this->compute_script_topological_order($id);
1055    }
1056
1057    uasort($todo, array('ScriptLoader', 'cmp_by_mode_and_order'));
1058
1059    $result = array( array(), array() );
1060    foreach( $todo as $id => $script)
1061    {
1062      $result[$script->load_mode-1][$id] = $script;
1063    }
1064    return array( self::do_combine($result[0],1), self::do_combine($result[1],2) );
1065  }
1066
1067  private static function do_combine($scripts, $load_mode)
1068  {
1069    global $conf;
1070    if (count($scripts)<2 or !$conf['template_combine_files'])
1071      return $scripts;
1072    $combiner = new FileCombiner('js');
1073    $result = array();
1074    foreach ($scripts as $script)
1075    {
1076      if ($script->is_remote())
1077      {
1078        if ( $combiner->combine( $out_file, $out_version) )
1079        {
1080          $results[] = new Script($load_mode, 'combi', $out_file, $out_version, array() );
1081        }
1082        $results[] = $script;
1083      }
1084      else
1085        $combiner->add( $script->path, $script->version );
1086    }
1087    if ( $combiner->combine( $out_file, $out_version) )
1088    {
1089      $results[] = new Script($load_mode, 'combi', $out_file, $out_version, array() );
1090    }
1091    return $results;
1092  }
1093
1094  // checks that if B depends on A, then B->load_mode >= A->load_mode in order to respect execution order
1095  private static function check_load_dep($scripts)
1096  {
1097    global $conf;
1098    do
1099    {
1100      $changed = false;
1101      foreach( $scripts as $id => $script)
1102      {
1103        $load = $script->load_mode;
1104        /*if ($load==0)
1105          continue;*/
1106        foreach( $script->precedents as $precedent)
1107        {
1108          if ( !isset($scripts[$precedent] ) )
1109            continue;
1110          if ( $scripts[$precedent]->load_mode > $load )
1111          {
1112            $scripts[$precedent]->load_mode = $load;
1113            $changed = true;
1114          }
1115          if ($load==2 && $scripts[$precedent]->load_mode==2 && ($scripts[$precedent]->is_remote() or !$conf['template_combine_files']) )
1116          {// we are async -> a predecessor cannot be async unlesss it can be merged; otherwise script execution order is not guaranteed
1117            $scripts[$precedent]->load_mode = 1;
1118            $changed = true;
1119          }
1120        }
1121      }
1122    }
1123    while ($changed);
1124  }
1125
1126
1127  private static function fill_well_known($id, $script)
1128  {
1129    if ( empty($script->path) && isset(self::$known_paths[$id]))
1130    {
1131      $script->path = self::$known_paths[$id];
1132    }
1133    if ( strncmp($id, 'jquery.', 7)==0 )
1134    {
1135      $required_ids = array('jquery');
1136
1137      if ( strncmp($id, 'jquery.ui.', 10)==0 )
1138      {
1139        if ( !isset(self::$ui_core_dependencies[$id]) )
1140          $required_ids = array_merge(array('jquery', 'jquery.ui'), array_keys(self::$ui_core_dependencies));
1141
1142        if ( empty($script->path) )
1143          $script->path = dirname(self::$known_paths['jquery.ui'])."/$id.min.js";
1144      }
1145      elseif ( strncmp($id, 'jquery.effects.', 15)==0 )
1146      {
1147        $required_ids = array('jquery', 'jquery.effects');
1148
1149        if ( empty($script->path) )
1150          $script->path = dirname(self::$known_paths['jquery.effects'])."/$id.min.js";
1151      }
1152
1153      foreach ($required_ids as $required_id)
1154      {
1155        if ( !in_array($required_id, $script->precedents ) )
1156          $script->precedents[] = $required_id;
1157      }
1158    }
1159  }
1160
1161  private function load_known_required_script($id, $load_mode)
1162  {
1163    if ( isset(self::$known_paths[$id]) or strncmp($id, 'jquery.ui.', 10)==0 or strncmp($id, 'jquery.effects.', 15)==0 )
1164    {
1165      $this->add($id, $load_mode, array(), null);
1166      return true;
1167    }
1168    return false;
1169  }
1170
1171  private function compute_script_topological_order($script_id, $recursion_limiter=0)
1172  {
1173    if (!isset($this->registered_scripts[$script_id]))
1174    {
1175      trigger_error("Undefined script $script_id is required by someone", E_USER_WARNING);
1176      return 0;
1177    }
1178    $recursion_limiter<5 or fatal_error("combined script circular dependency");
1179    $script = & $this->registered_scripts[$script_id];
1180    if (isset($script->extra['order']))
1181      return $script->extra['order'];
1182    if (count($script->precedents) == 0)
1183      return ($script->extra['order'] = 0);
1184    $max = 0;
1185    foreach( $script->precedents as $precedent)
1186      $max = max($max, $this->compute_script_topological_order($precedent, $recursion_limiter+1) );
1187    $max++;
1188    return ($script->extra['order'] = $max);
1189  }
1190
1191  private static function cmp_by_mode_and_order($s1, $s2)
1192  {
1193    $ret = $s1->load_mode - $s2->load_mode;
1194    if ($ret) return $ret;
1195
1196    $ret = $s1->extra['order'] - $s2->extra['order'];
1197    if ($ret) return $ret;
1198
1199    if ($s1->extra['order']==0 and ($s1->is_remote() xor $s2->is_remote()) )
1200    {
1201      return $s1->is_remote() ? -1 : 1;
1202    }
1203    return strcmp($s1->id,$s2->id);
1204  }
1205}
1206
1207
1208/*Allows merging of javascript and css files into a single one.*/
1209final class FileCombiner
1210{
1211  const OUT_SUB_DIR = PWG_COMBINED_DIR;
1212  private $type; // js or css
1213  private $files = array();
1214  private $versions = array();
1215
1216  function FileCombiner($type)
1217  {
1218    $this->type = $type;
1219  }
1220
1221  static function clear_combined_files()
1222  {
1223    $dir = opendir(PHPWG_ROOT_PATH.self::OUT_SUB_DIR);
1224    while ($file = readdir($dir))
1225    {
1226      if ( get_extension($file)=='js' || get_extension($file)=='css')
1227        unlink(PHPWG_ROOT_PATH.self::OUT_SUB_DIR.$file);
1228    }
1229    closedir($dir);
1230  }
1231
1232  function add($file, $version)
1233  {
1234    $this->files[] = $file;
1235    $this->versions[] = $version;
1236  }
1237
1238  function clear()
1239  {
1240    $this->files = array();
1241    $this->versions = array();
1242  }
1243
1244  function combine(&$out_file, &$out_version)
1245  {
1246    if (count($this->files) == 0)
1247    {
1248      return false;
1249    }
1250    if (count($this->files) == 1)
1251    {
1252      $out_file = $this->files[0];
1253      $out_version = $this->versions[0];
1254      $this->clear();
1255      return 1;
1256    }
1257
1258    $is_css = $this->type == "css";
1259    global $conf;
1260    $key = array();
1261    if ($is_css)
1262      $key[] = get_absolute_root_url(false);//because we modify bg url
1263    for ($i=0; $i<count($this->files); $i++)
1264    {
1265      $key[] = $this->files[$i];
1266      $key[] = $this->versions[$i];
1267      if ($conf['template_compile_check']) $key[] = filemtime( PHPWG_ROOT_PATH . $this->files[$i] );
1268    }
1269    $key = join('>', $key);
1270
1271    $file = base_convert(crc32($key),10,36);
1272    $file = self::OUT_SUB_DIR . $file . '.' . $this->type;
1273
1274    $exists = file_exists( PHPWG_ROOT_PATH . $file );
1275    if ($exists)
1276    {
1277      $is_reload =
1278        (isset($_SERVER['HTTP_CACHE_CONTROL']) && strpos($_SERVER['HTTP_CACHE_CONTROL'], 'max-age=0') !== false)
1279        || (isset($_SERVER['HTTP_PRAGMA']) && strpos($_SERVER['HTTP_PRAGMA'], 'no-cache'));
1280      if (is_admin() && $is_reload)
1281      {// the user pressed F5 in the browser
1282        if ($is_css || $conf['template_compile_check']==false)
1283          $exists = false; // we foce regeneration of css because @import sub-files are never checked for modification
1284      }
1285    }
1286
1287    if ($exists)
1288    {
1289      $out_file = $file;
1290      $out_version = false;
1291      $this->clear();
1292      return 2;
1293    }
1294
1295    $output = '';
1296    foreach ($this->files as $input_file)
1297    {
1298      $output .= "/*BEGIN $input_file */\n";
1299      if ($is_css)
1300        $output .= self::process_css($input_file);
1301      else
1302        $output .= self::process_js($input_file);
1303      $output .= "\n";
1304    }
1305
1306    file_put_contents( PHPWG_ROOT_PATH . $file,  $output );
1307    $out_file = $file;
1308    $out_version = false;
1309    $this->clear();
1310    return 2;
1311  }
1312
1313  private static function process_js($file)
1314  {
1315    $js = file_get_contents(PHPWG_ROOT_PATH . $file);
1316    if (strpos($file, '.min')===false and strpos($file, '.packed')===false )
1317    {
1318      require_once(PHPWG_ROOT_PATH.'include/jsmin.class.php');
1319      try { $js = JSMin::minify($js); } catch(Exception $e) {}
1320    }
1321    return trim($js, " \t\r\n;").";\n";
1322  }
1323
1324  private static function process_css($file)
1325  {
1326    $css = self::process_css_rec($file);
1327    require_once(PHPWG_ROOT_PATH.'include/cssmin.class.php');
1328    $css = CssMin::minify($css, array('emulate-css3-variables'=>false));
1329    $css = trigger_event('combined_css_postfilter', $css);
1330    return $css;
1331  }
1332
1333  private static function process_css_rec($file)
1334  {
1335    static $PATTERN = "#url\(\s*['|\"]{0,1}(.*?)['|\"]{0,1}\s*\)#";
1336    $css = file_get_contents(PHPWG_ROOT_PATH . $file);
1337    if (preg_match_all($PATTERN, $css, $matches, PREG_SET_ORDER))
1338    {
1339      $search = $replace = array();
1340      foreach ($matches as $match)
1341      {
1342        if ( !url_is_remote($match[1]) || $match[1][0] != '/')
1343        {
1344          $relative = dirname($file) . "/$match[1]";
1345          $search[] = $match[0];
1346          $replace[] = 'url('.embellish_url(get_absolute_root_url(false).$relative).')';
1347        }
1348      }
1349      $css = str_replace($search, $replace, $css);
1350    }
1351
1352    $imports = preg_match_all("#@import\s*['|\"]{0,1}(.*?)['|\"]{0,1};#", $css, $matches, PREG_SET_ORDER);
1353    if ($imports)
1354    {
1355      $search = $replace = array();
1356      foreach ($matches as $match)
1357      {
1358        $search[] = $match[0];
1359        $replace[] = self::process_css_rec(dirname($file) . "/$match[1]");
1360      }
1361      $css = str_replace($search, $replace, $css);
1362    }
1363    return $css;
1364  }
1365}
1366
1367?>
Note: See TracBrowser for help on using the repository browser.