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

Last change on this file since 19703 was 19703, checked in by plg, 11 years ago

update Piwigo headers to 2013 (the end of the world didn't occur as expected on r12922)

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