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

Last change on this file since 25464 was 25464, checked in by rvelices, 10 years ago

bug 2963: Ability to create css/js smarty templates - fixes

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