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

Last change on this file since 29713 was 29636, checked in by plg, 10 years ago

bug 3119 fixed: patch by mmoy, allow @import url(http://...); directives in minified CSS file.

  • Property svn:eol-style set to LF
File size: 56.9 KB
RevLine 
[2216]1<?php
2// +-----------------------------------------------------------------------+
[8728]3// | Piwigo - a PHP based photo gallery                                    |
[2297]4// +-----------------------------------------------------------------------+
[26718]5// | Copyright(C) 2008-2013 Piwigo Team                  http://piwigo.org |
[2297]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// +-----------------------------------------------------------------------+
[2216]23
[25812]24/**
25 * @package template
26 */
27
[27184]28require_once( PHPWG_ROOT_PATH .'include/smarty/libs/Smarty.class.php');
[25812]29
30
31/** default rank for buttons */
[23263]32define('BUTTONS_RANK_NEUTRAL', 50);
[2216]33
[25812]34/**
35 * This a wrapper arround Smarty classes proving various custom mechanisms for templates.
36 */
37class Template
38{
39  /** @var Smarty */
[2216]40  var $smarty;
[25812]41  /** @var string */
[2216]42  var $output = '';
43
[25812]44  /** @var string[] - Hash of filenames for each template handle. */
[2216]45  var $files = array();
[25812]46  /** @var string[] - Template extents filenames for each template handle. */
[2643]47  var $extents = array();
[25812]48  /** @var array - Templates prefilter from external sources (plugins) */
[3927]49  var $external_filters = array();
[5177]50
[25812]51  /** @var string - Content to add before </head> tag */
[2334]52  var $html_head_elements = array();
[25812]53  /** @var string - Runtime CSS rules */
[12920]54  private $html_style = '';
[2334]55
[25812]56  /** @const string */
[7987]57  const COMBINED_SCRIPTS_TAG = '<!-- COMBINED_SCRIPTS -->';
[25812]58  /** @var ScriptLoader */
[7975]59  var $scriptLoader;
60
[25812]61  /** @const string */
[7987]62  const COMBINED_CSS_TAG = '<!-- COMBINED_CSS -->';
[25812]63  /** @var CssLoader */
[25506]64  var $cssLoader;
[21818]65
[25812]66  /** @var array - Runtime buttons on picture page */
[18760]67  var $picture_buttons = array();
[25812]68  /** @var array - Runtime buttons on index page */
[18760]69  var $index_buttons = array();
[7995]70
[25812]71
72  /**
73   * @var string $root
74   * @var string $theme
75   * @var string $path
76   */
[25815]77  function __construct($root=".", $theme="", $path="template")
[2216]78  {
[2632]79    global $conf, $lang_info;
[2216]80
[23476]81    SmartyException::$escape = false;
82
[7975]83    $this->scriptLoader = new ScriptLoader;
[25506]84    $this->cssLoader = new CssLoader;
[27184]85    $this->smarty = new Smarty;
[2216]86    $this->smarty->debugging = $conf['debug_template'];
[23425]87    if (!$this->smarty->debugging)
[25812]88    {
[23425]89      $this->smarty->error_reporting = error_reporting() & ~E_NOTICE;
[25812]90    }
[5208]91    $this->smarty->compile_check = $conf['template_compile_check'];
92    $this->smarty->force_compile = $conf['template_force_compile'];
[2216]93
[12802]94    if (!isset($conf['data_dir_checked']))
[5985]95    {
[12802]96      $dir = PHPWG_ROOT_PATH.$conf['data_location'];
97      mkgetdir($dir, MKGETDIR_DEFAULT&~MKGETDIR_DIE_ON_ERROR);
98      if (!is_writable($dir))
[5998]99      {
100        load_language('admin.lang');
101        fatal_error(
[25005]102          l10n(
103            'Give write access (chmod 777) to "%s" directory at the root of your Piwigo installation',
[12802]104            $conf['data_location']
[5998]105            ),
106          l10n('an error happened'),
107          false // show trace
108          );
109      }
[5999]110      if (function_exists('pwg_query')) {
[12802]111        conf_update_param('data_dir_checked', 1);
[5999]112      }
[5985]113    }
[7995]114
[12802]115    $compile_dir = PHPWG_ROOT_PATH.$conf['data_location'].'templates_c';
[2497]116    mkgetdir( $compile_dir );
[2216]117
[23384]118    $this->smarty->setCompileDir($compile_dir);
[2216]119
[23384]120    $this->smarty->assign( 'pwg', new PwgTemplateAdapter() );
[23425]121    $this->smarty->registerPlugin('modifiercompiler', 'translate', array('Template', 'modcompiler_translate') );
[23476]122    $this->smarty->registerPlugin('modifiercompiler', 'translate_dec', array('Template', 'modcompiler_translate_dec') );
[23425]123    $this->smarty->registerPlugin('modifier', 'explode', array('Template', 'mod_explode') );
[28703]124    $this->smarty->registerPlugin('modifier', 'ternary', array('Template', 'mod_ternary') );
[24988]125    $this->smarty->registerPlugin('modifier', 'get_extent', array($this, 'get_extent') );
[23425]126    $this->smarty->registerPlugin('block', 'html_head', array($this, 'block_html_head') );
127    $this->smarty->registerPlugin('block', 'html_style', array($this, 'block_html_style') );
128    $this->smarty->registerPlugin('function', 'combine_script', array($this, 'func_combine_script') );
129    $this->smarty->registerPlugin('function', 'get_combined_scripts', array($this, 'func_get_combined_scripts') );
130    $this->smarty->registerPlugin('function', 'combine_css', array($this, 'func_combine_css') );
131    $this->smarty->registerPlugin('function', 'define_derivative', array($this, 'func_define_derivative') );
132    $this->smarty->registerPlugin('compiler', 'get_combined_css', array($this, 'func_get_combined_css') );
133    $this->smarty->registerPlugin('block', 'footer_script', array($this, 'block_footer_script') );
134    $this->smarty->registerFilter('pre', array('Template', 'prefilter_white_space') );
[2334]135    if ( $conf['compiled_template_cache_language'] )
136    {
[23688]137      $this->smarty->registerFilter('post', array('Template', 'postfilter_language') );
[2334]138    }
[2216]139
[23384]140    $this->smarty->setTemplateDir(array());
[5177]141    if ( !empty($theme) )
[5208]142    {
[5177]143      $this->set_theme($root, $theme, $path);
[19696]144      if (!defined('IN_ADMIN'))
145      {
146        $this->set_prefilter( 'header', array('Template', 'prefilter_local_css') );
147      }
[5208]148    }
[5177]149    else
150      $this->set_template_dir($root);
[2216]151
[28917]152    if (isset($lang_info['code']) and !isset($lang_info['jquery_code']))
153    {
154      $lang_info['jquery_code'] = $lang_info['code'];
155    }
156
157    if (isset($lang_info['jquery_code']) and !isset($lang_info['plupload_code']))
158    {
159      $lang_info['plupload_code'] = str_replace('-', '_', $lang_info['jquery_code']);
160    }
161   
[2632]162    $this->smarty->assign('lang_info', $lang_info);
163
[3169]164    if (!defined('IN_ADMIN') and isset($conf['extents_for_templates']))
[2643]165    {
166      $tpl_extents = unserialize($conf['extents_for_templates']);
[5126]167      $this->set_extents($tpl_extents, './template-extension/', true, $theme);
[2643]168    }
[2216]169  }
170
[2278]171  /**
[25812]172   * Loads theme's parameters.
173   *
174   * @param string $root
175   * @param string $theme
176   * @param string $path
177   * @param bool $load_css
178   * @param bool $load_local_head
[2278]179   */
[29389]180  function set_theme($root, $theme, $path, $load_css=true, $load_local_head=true, $colorscheme='dark')
[5123]181  {
182    $this->set_template_dir($root.'/'.$theme.'/'.$path);
183
[5446]184    $themeconf = $this->load_themeconf($root.'/'.$theme);
[5123]185
[5154]186    if (isset($themeconf['parent']) and $themeconf['parent'] != $theme)
[5123]187    {
[6006]188      $this->set_theme(
189        $root,
190        $themeconf['parent'],
191        $path,
192        isset($themeconf['load_parent_css']) ? $themeconf['load_parent_css'] : $load_css,
193        isset($themeconf['load_parent_local_head']) ? $themeconf['load_parent_local_head'] : $load_local_head
194      );
[5123]195    }
196
[5991]197    $tpl_var = array(
198      'id' => $theme,
199      'load_css' => $load_css,
200    );
[6006]201    if (!empty($themeconf['local_head']) and $load_local_head)
[5123]202    {
[5177]203      $tpl_var['local_head'] = realpath($root.'/'.$theme.'/'.$themeconf['local_head'] );
[5123]204    }
[6318]205    $themeconf['id'] = $theme;
[29389]206
207    if (!isset($themeconf['colorscheme']))
208    {
209      $themeconf['colorscheme'] = $colorscheme;
210    }
211   
[5123]212    $this->smarty->append('themes', $tpl_var);
213    $this->smarty->append('themeconf', $themeconf, true);
214  }
215
[5126]216  /**
[25812]217   * Adds template directory for this Template object.
218   * Also set compile id if not exists.
219   *
220   * @param string $dir
[5126]221   */
[2278]222  function set_template_dir($dir)
223  {
[23384]224    $this->smarty->addTemplateDir($dir);
[5123]225
226    if (!isset($this->smarty->compile_id))
227    {
[23425]228      $compile_id = "1";
229      $compile_id .= ($real_dir = realpath($dir))===false ? $dir : $real_dir;
230      $this->smarty->compile_id = base_convert(crc32($compile_id), 10, 36 );
[5123]231    }
[2278]232  }
233
234  /**
235   * Gets the template root directory for this Template object.
[25812]236   *
237   * @return string
[2278]238   */
239  function get_template_dir()
240  {
[23384]241    return $this->smarty->getTemplateDir();
[2278]242  }
243
244  /**
245   * Deletes all compiled templates.
246   */
247  function delete_compiled_templates()
248  {
249      $save_compile_id = $this->smarty->compile_id;
250      $this->smarty->compile_id = null;
[23384]251      $this->smarty->clearCompiledTemplate();
[2278]252      $this->smarty->compile_id = $save_compile_id;
[23384]253      file_put_contents($this->smarty->getCompileDir().'/index.htm', 'Not allowed!');
[2278]254  }
255
[25812]256  /**
257   * Returns theme's parameter.
258   *
259   * @param string $val
260   * @return mixed
261   */
[2216]262  function get_themeconf($val)
263  {
[23384]264    $tc = $this->smarty->getTemplateVars('themeconf');
[2216]265    return isset($tc[$val]) ? $tc[$val] : '';
266  }
267
268  /**
269   * Sets the template filename for handle.
[25812]270   *
271   * @param string $handle
272   * @param string $filename
273   * @return bool
[2216]274   */
275  function set_filename($handle, $filename)
276  {
277    return $this->set_filenames( array($handle=>$filename) );
278  }
279
280  /**
[25812]281   * Sets the template filenames for handles.
282   *
283   * @param string[] $filename_array hashmap of handle=>filename
284   * @return true
[2216]285   */
286  function set_filenames($filename_array)
287  {
288    if (!is_array($filename_array))
289    {
290      return false;
291    }
292    reset($filename_array);
293    while(list($handle, $filename) = each($filename_array))
294    {
295      if (is_null($filename))
[2643]296      {
297        unset($this->files[$handle]);
298      }
[2216]299      else
[2434]300      {
[2716]301        $this->files[$handle] = $this->get_extent($filename, $handle);
[2434]302      }
[2216]303    }
304    return true;
305  }
[2476]306
[2643]307  /**
308   * Sets template extention filename for handles.
[25812]309   *
310   * @param string $filename
311   * @param mixed $param
312   * @param string $dir
313   * @param bool $overwrite
314   * @param string $theme
315   * @return bool
[2643]316   */
[5126]317  function set_extent($filename, $param, $dir='', $overwrite=true, $theme='N/A')
[2643]318  {
319    return $this->set_extents(array($filename => $param), $dir, $overwrite);
320  }
321
322  /**
[2699]323   * Sets template extentions filenames for handles.
[25812]324   *
325   * @param string[] $filename_array hashmap of handle=>filename
326   * @param string $dir
327   * @param bool $overwrite
328   * @param string $theme
329   * @return bool
[2643]330   */
[5126]331  function set_extents($filename_array, $dir='', $overwrite=true, $theme='N/A')
[2643]332  {
333    if (!is_array($filename_array))
334    {
335      return false;
336    }
337    foreach ($filename_array as $filename => $value)
338    {
339      if (is_array($value))
340      {
341        $handle = $value[0];
342        $param = $value[1];
[5126]343        $thm = $value[2];
[2643]344      }
345      elseif (is_string($value))
346      {
347        $handle = $value;
348        $param = 'N/A';
[5126]349        $thm = 'N/A';
[2643]350      }
351      else
352      {
353        return false;
354      }
355
[3207]356      if ((stripos(implode('',array_keys($_GET)), '/'.$param) !== false or $param == 'N/A')
[5434]357        and ($thm == $theme or $thm == 'N/A')
[2643]358        and (!isset($this->extents[$handle]) or $overwrite)
359        and file_exists($dir . $filename))
360      {
361        $this->extents[$handle] = realpath($dir . $filename);
362      }
363    }
364    return true;
365  }
[2790]366
[25812]367  /**
368   * Returns template extension if exists.
369   *
370   * @param string $filename should be empty!
371   * @param string $handle
372   * @return string
373   */
[2716]374  function get_extent($filename='', $handle='')
375  {
376    if (isset($this->extents[$handle]))
377    {
378      $filename = $this->extents[$handle];
379    }
380    return $filename;
381  }
[2643]382
[25812]383  /**
384   * Assigns a template variable.
385   * @see http://www.smarty.net/manual/en/api.assign.php
386   *
387   * @param string|array $tpl_var can be a var name or a hashmap of variables
388   *    (in this case, do not use the _$value_ parameter)
389   * @param mixed $value
390   */
391  function assign($tpl_var, $value=null)
[2286]392  {
393    $this->smarty->assign( $tpl_var, $value );
[2290]394  }
[2286]395
[2216]396  /**
[25812]397   * Defines _$varname_ as the compiled result of _$handle_.
398   * This can be used to effectively include a template in another template.
399   * This is equivalent to assign($varname, $this->parse($handle, true)).
400   *
401   * @param string $varname
402   * @param string $handle
403   * @return true
[2286]404   */
405  function assign_var_from_handle($varname, $handle)
406  {
407    $this->assign($varname, $this->parse($handle, true));
408    return true;
409  }
410
[25812]411  /**
412   * Appends a new value in a template array variable, the variable is created if needed.
413   * @see http://www.smarty.net/manual/en/api.append.php
414   *
415   * @param string $tpl_var
416   * @param mixed $value
417   * @param bool $merge
418   */
[2286]419  function append($tpl_var, $value=null, $merge=false)
420  {
421    $this->smarty->append( $tpl_var, $value, $merge );
422  }
423
424  /**
[25812]425   * Performs a string concatenation.
426   *
427   * @param string $tpl_var
428   * @param string $value
[2286]429   */
430  function concat($tpl_var, $value)
431  {
[23425]432    $this->assign($tpl_var,
433      $this->smarty->getTemplateVars($tpl_var) . $value);
[2286]434  }
435
[25812]436  /**
437   * Removes an assigned template variable.
438   * @see http://www.smarty.net/manual/en/api.clear_assign.php
439   *
440   * @param string $tpl_var
441   */
[2286]442  function clear_assign($tpl_var)
443  {
[23476]444    $this->smarty->clearAssign( $tpl_var );
[2286]445  }
446
[25812]447  /**
448   * Returns an assigned template variable.
449   * @see http://www.smarty.net/manual/en/api.get_template_vars.php
450   *
451   * @param string $tpl_var
452   */
453  function get_template_vars($tpl_var=null)
[2286]454  {
[25812]455    return $this->smarty->getTemplateVars( $tpl_var );
[2286]456  }
457
458  /**
[25812]459   * Loads the template file of the handle, compiles it and appends the result to the output
460   * (or returns it if _$return_ is true).
461   *
462   * @param string $handle
463   * @param bool $return
464   * @return null|string
[2216]465   */
466  function parse($handle, $return=false)
467  {
468    if ( !isset($this->files[$handle]) )
469    {
[2502]470      fatal_error("Template->parse(): Couldn't load template file for handle $handle");
[2216]471    }
472
[2290]473    $this->smarty->assign( 'ROOT_URL', get_root_url() );
[2334]474
[3928]475    $save_compile_id = $this->smarty->compile_id;
476    $this->load_external_filters($handle);
[3927]477
[2334]478    global $conf, $lang_info;
479    if ( $conf['compiled_template_cache_language'] and isset($lang_info['code']) )
480    {
[23476]481      $this->smarty->compile_id .= '_'.$lang_info['code'];
[2334]482    }
[2476]483
[23384]484    $v = $this->smarty->fetch($this->files[$handle]);
[2476]485
[3928]486    $this->smarty->compile_id = $save_compile_id;
487    $this->unload_external_filters($handle);
[2476]488
[2231]489    if ($return)
[2216]490    {
[2231]491      return $v;
[2216]492    }
[2231]493    $this->output .= $v;
[2216]494  }
495
[2231]496  /**
[25812]497   * Loads the template file of the handle, compiles it and appends the result to the output,
498   * then sends the output to the browser.
499   *
500   * @param string $handle
[2231]501   */
502  function pparse($handle)
503  {
504    $this->parse($handle, false);
[2334]505    $this->flush();
506  }
507
[25812]508  /**
509   * Load and compile JS & CSS into the template and sends the output to the browser.
510   */
[2334]511  function flush()
512  {
[7975]513    if (!$this->scriptLoader->did_head())
514    {
[7987]515      $pos = strpos( $this->output, self::COMBINED_SCRIPTS_TAG );
[7975]516      if ($pos !== false)
517      {
518          $scripts = $this->scriptLoader->get_head_scripts();
519          $content = array();
[8012]520          foreach ($scripts as $script)
[7975]521          {
522              $content[]=
523                  '<script type="text/javascript" src="'
[8170]524                  . self::make_script_src($script)
[7975]525                  .'"></script>';
526          }
527
[23588]528          $this->output = substr_replace( $this->output, implode( "\n", $content ), $pos, strlen(self::COMBINED_SCRIPTS_TAG) );
[7975]529      } //else maybe error or warning ?
530    }
[7995]531
[25506]532    $css = $this->cssLoader->get_css();
533
534    $content = array();
535    foreach( $css as $combi )
[7987]536    {
[25506]537      $href = embellish_url(get_root_url().$combi->path);
538      if ($combi->version !== false)
539        $href .= '?v' . ($combi->version ? $combi->version : PHPWG_VERSION);
540      // trigger the event for eventual use of a cdn
[28587]541      $href = trigger_change('combined_css', $href, $combi);
[25506]542      $content[] = '<link rel="stylesheet" type="text/css" href="'.$href.'">';
[7987]543    }
[25506]544    $this->output = str_replace(self::COMBINED_CSS_TAG,
545        implode( "\n", $content ),
546        $this->output );
547    $this->cssLoader->clear();
[7987]548
[12920]549    if ( count($this->html_head_elements) || strlen($this->html_style) )
[2334]550    {
551      $search = "\n</head>";
552      $pos = strpos( $this->output, $search );
553      if ($pos !== false)
554      {
[12920]555        $rep = "\n".implode( "\n", $this->html_head_elements );
556        if (strlen($this->html_style))
557        {
[12955]558          $rep.='<style type="text/css">'.$this->html_style.'</style>';
[12920]559        }
560        $this->output = substr_replace( $this->output, $rep, $pos, 0 );
[2334]561      } //else maybe error or warning ?
562      $this->html_head_elements = array();
[12920]563      $this->html_style = '';
[2334]564    }
[3927]565
[2231]566    echo $this->output;
567    $this->output='';
568  }
569
[25812]570  /**
571   * Same as flush() but with optional debugging.
572   * @see Template::flush()
573   */
[2216]574  function p()
575  {
[2334]576    $this->flush();
[2231]577
578    if ($this->smarty->debugging)
579    {
580      global $t2;
581      $this->smarty->assign(
582        array(
583        'AAAA_DEBUG_TOTAL_TIME__' => get_elapsed_time($t2, get_moment())
584        )
585        );
[23384]586      Smarty_Internal_Debug::display_debug($this->smarty);
[2231]587    }
[2216]588  }
589
[25812]590  /**
591   * Eval a temp string to retrieve the original PHP value.
592   *
593   * @param string $str
594   * @return mixed
595   */
[25231]596  static function get_php_str_val($str)
[23425]597  {
598    if (is_string($str) && strlen($str)>1)
599    {
600      if ( ($str[0]=='\'' && $str[strlen($str)-1]=='\'')
601        || ($str[0]=='"' && $str[strlen($str)-1]=='"'))
602      {
603        eval('$tmp='.$str.';');
604        return $tmp;
605      }
606    }
607    return null;
608  }
609
[2216]610  /**
[25812]611   * "translate" variable modifier.
[28600]612   * Usage :
[25812]613   *    - {'Comment'|translate}
614   *    - {'%d comments'|translate:$count}
615   * @see l10n()
616   *
617   * @param array $params
618   * @return string
[2216]619   */
[23425]620  static function modcompiler_translate($params)
[2216]621  {
[23425]622    global $conf, $lang;
[24988]623
624    switch (count($params))
[23425]625    {
[24988]626    case 1:
627      if ($conf['compiled_template_cache_language']
628        && ($key=self::get_php_str_val($params[0])) !== null
629        && isset($lang[$key])
630      ) {
[23425]631        return var_export($lang[$key], true);
[24988]632      }
633      return 'l10n('.$params[0].')';
[25462]634
[24988]635    default:
636      if ($conf['compiled_template_cache_language'])
637      {
638        $ret = 'sprintf(';
639        $ret .= self::modcompiler_translate( array($params[0]) );
640        $ret .= ','. implode(',', array_slice($params, 1));
641        $ret .= ')';
642        return $ret;
643      }
644      return 'l10n('.$params[0].','.implode(',', array_slice($params, 1)).')';
[23425]645    }
[2216]646  }
647
[25812]648  /**
649   * "translate_dec" variable modifier.
650   * Usage :
651   *    - {$count|translate_dec:'%d comment':'%d comments'}
652   * @see l10n_dec()
653   *
654   * @param array $params
655   * @return string
656   */
[23476]657  static function modcompiler_translate_dec($params)
658  {
659    global $conf, $lang, $lang_info;
[24988]660    if ($conf['compiled_template_cache_language'])
[23476]661    {
662      $ret = 'sprintf(';
663      if ($lang_info['zero_plural'])
664      {
665        $ret .= '($tmp=('.$params[0].'))>1||$tmp==0';
666      }
667      else
668      {
[23688]669        $ret .= '($tmp=('.$params[0].'))>1';
[23476]670      }
671      $ret .= '?';
672      $ret .= self::modcompiler_translate( array($params[2]) );
673      $ret .= ':';
674      $ret .= self::modcompiler_translate( array($params[1]) );
[23688]675      $ret .= ',$tmp';
[23476]676      $ret .= ')';
677      return $ret;
678    }
679    return 'l10n_dec('.$params[1].','.$params[2].','.$params[0].')';
680  }
681
[2216]682  /**
[25812]683   * "explode" variable modifier.
684   * Usage :
[28703]685   *    - {assign var=valueExploded value=$value|explode:','}
[25812]686   *
687   * @param string $text
688   * @param string $delimiter
689   * @return array
[2216]690   */
[2536]691  static function mod_explode($text, $delimiter=',')
[2216]692  {
[2286]693    return explode($delimiter, $text);
[2216]694  }
[28703]695 
696  /**
697   * ternary variable modifier.
698   * Usage :
699   *    - {$variable|ternary:'yes':'no'}
700   *
701   * @param mixed $param
702   * @param mixed $true
703   * @param mixed $false
704   * @return mixed
705   */
706  static function mod_ternary($param, $true, $false)
707  {
708    return $param ? $true : $false;
709  }
[2476]710
[2334]711  /**
[25812]712   * The "html_head" block allows to add content just before
713   * </head> element in the output after the head has been parsed.
714   *
715   * @param array $params (unused)
716   * @param string $content
[2334]717   */
[17021]718  function block_html_head($params, $content)
[2334]719  {
720    $content = trim($content);
721    if ( !empty($content) )
722    { // second call
[2627]723      $this->html_head_elements[] = $content;
[2334]724    }
725  }
[2476]726
[25812]727  /**
728   * The "html_style" block allows to add CSS juste before
729   * </head> element in the output after the head has been parsed.
730   *
731   * @param array $params (unused)
732   * @param string $content
733   */
[17021]734  function block_html_style($params, $content)
[12920]735  {
736    $content = trim($content);
737    if ( !empty($content) )
738    { // second call
[20370]739      $this->html_style .= "\n".$content;
[12920]740    }
741  }
742
[25812]743  /**
744   * The "define_derivative" function allows to define derivative from tpl file.
745   * It assigns a DerivativeParams object to _name_ template variable.
746   *
747   * @param array $params
748   *    - name (required)
749   *    - type (optional)
750   *    - width (required if type is empty)
751   *    - height (required if type is empty)
752   *    - crop (optional, used if type is empty)
753   *    - min_height (optional, used with crop)
754   *    - min_height (optional, used with crop)
755   * @param Smarty $smarty
756   */
[23588]757  function func_define_derivative($params, $smarty)
[12954]758  {
[13871]759    !empty($params['name']) or fatal_error('define_derivative missing name');
[12954]760    if (isset($params['type']))
761    {
762      $derivative = ImageStdParams::get_by_type($params['type']);
[23588]763      $smarty->assign( $params['name'], $derivative);
[12954]764      return;
765    }
[13871]766    !empty($params['width']) or fatal_error('define_derivative missing width');
767    !empty($params['height']) or fatal_error('define_derivative missing height');
[12954]768
[13021]769    $w = intval($params['width']);
770    $h = intval($params['height']);
771    $crop = 0;
772    $minw=null;
773    $minh=null;
[18630]774
[12954]775    if (isset($params['crop']))
776    {
777      if (is_bool($params['crop']))
778      {
[13021]779        $crop = $params['crop'] ? 1:0;
[12954]780      }
781      else
782      {
[13021]783        $crop = round($params['crop']/100, 2);
[12954]784      }
785
[13021]786      if ($crop)
[12954]787      {
[13021]788        $minw = empty($params['min_width']) ? $w : intval($params['min_width']);
[13871]789        $minw <= $w or fatal_error('define_derivative invalid min_width');
[13021]790        $minh = empty($params['min_height']) ? $h : intval($params['min_height']);
[13871]791        $minh <= $h or fatal_error('define_derivative invalid min_height');
[12954]792      }
793    }
794
[23588]795    $smarty->assign( $params['name'], ImageStdParams::get_custom($w, $h, $crop, $minw, $minh) );
[12954]796  }
797
[25812]798  /**
799   * The "combine_script" functions allows inclusion of a javascript file in the current page.
800   * The engine will combine several js files into a single one.
801   *
802   * @param array $params
803   *   - id (required)
804   *   - path (required)
805   *   - load (optional) 'header', 'footer' or 'async'
806   *   - require (optional) comma separated list of script ids required to be loaded
807   *     and executed before this one
808   *   - version (optional) used to force a browser refresh
809   */
[17021]810  function func_combine_script($params)
[7975]811  {
812    if (!isset($params['id']))
813    {
[23384]814      trigger_error("combine_script: missing 'id' parameter", E_USER_ERROR);
[7975]815    }
816    $load = 0;
817    if (isset($params['load']))
818    {
819      switch ($params['load'])
820      {
821        case 'header': break;
822        case 'footer': $load=1; break;
823        case 'async': $load=2; break;
[23384]824        default: trigger_error("combine_script: invalid 'load' parameter", E_USER_ERROR);
[7975]825      }
826    }
[18775]827
[7995]828    $this->scriptLoader->add( $params['id'], $load,
829      empty($params['require']) ? array() : explode( ',', $params['require'] ),
830      @$params['path'],
[26718]831      isset($params['version']) ? $params['version'] : 0,
832      @$params['template']);
[7975]833  }
[2513]834
[25812]835  /**
836   * The "get_combined_scripts" function returns HTML tag of combined scripts.
837   * It can returns a placeholder for delayed JS files combination and minification.
838   *
839   * @param array $params
840   *    - load (required)
841   */
[17021]842  function func_get_combined_scripts($params)
[7975]843  {
844    if (!isset($params['load']))
845    {
[23384]846      trigger_error("get_combined_scripts: missing 'load' parameter", E_USER_ERROR);
[7975]847    }
848    $load = $params['load']=='header' ? 0 : 1;
849    $content = array();
[7995]850
[7975]851    if ($load==0)
852    {
[7987]853      return self::COMBINED_SCRIPTS_TAG;
[7975]854    }
855    else
856    {
857      $scripts = $this->scriptLoader->get_footer_scripts();
[8012]858      foreach ($scripts[0] as $script)
[7975]859      {
860        $content[]=
861          '<script type="text/javascript" src="'
[8012]862          . self::make_script_src($script)
[7975]863          .'"></script>';
864      }
[7995]865      if (count($this->scriptLoader->inline_scripts))
[7975]866      {
[8401]867        $content[]= '<script type="text/javascript">//<![CDATA[
[8299]868';
[7995]869        $content = array_merge($content, $this->scriptLoader->inline_scripts);
[8299]870        $content[]= '//]]></script>';
[7975]871      }
872
873      if (count($scripts[1]))
874      {
875        $content[]= '<script type="text/javascript">';
876        $content[]= '(function() {
[8725]877var s,after = document.getElementsByTagName(\'script\')[document.getElementsByTagName(\'script\').length-1];';
[7975]878        foreach ($scripts[1] as $id => $script)
879        {
880          $content[]=
[8378]881            's=document.createElement(\'script\'); s.type=\'text/javascript\'; s.async=true; s.src=\''
[8012]882            . self::make_script_src($script)
[7975]883            .'\';';
884          $content[]= 'after = after.parentNode.insertBefore(s, after);';
885        }
886        $content[]= '})();';
887        $content[]= '</script>';
888      }
889    }
890    return implode("\n", $content);
891  }
892
[25812]893  /**
894   * Returns clean relative URL to script file.
895   *
896   * @param Combinable $script
897   * @return string
898   */
899  private static function make_script_src($script)
[7975]900  {
901    $ret = '';
[8012]902    if ( $script->is_remote() )
[7975]903      $ret = $script->path;
904    else
905    {
[13240]906      $ret = get_root_url().$script->path;
[7975]907      if ($script->version!==false)
908      {
909        $ret.= '?v'. ($script->version ? $script->version : PHPWG_VERSION);
910      }
911    }
[8012]912    // trigger the event for eventual use of a cdn
[28587]913    $ret = trigger_change('combined_script', $ret, $script);
[13240]914    return embellish_url($ret);
[7975]915  }
916
[25812]917  /**
918   * The "footer_script" block allows to add runtime script in the HTML page.
919   *
920   * @param array $params
921   *    - require (optional) comma separated list of script ids
922   * @param string $content
923   */
[17021]924  function block_footer_script($params, $content)
[7975]925  {
926    $content = trim($content);
927    if ( !empty($content) )
928    { // second call
[18775]929
[9580]930      $this->scriptLoader->add_inline(
931        $content,
932        empty($params['require']) ? array() : explode(',', $params['require'])
933      );
[7975]934    }
935  }
[7995]936
[8506]937  /**
[25812]938   * The "combine_css" function allows inclusion of a css file in the current page.
939   * The engine will combine several css files into a single one.
940   *
941   * @param array $params
942   *    - id (optional) used to deal with multiple inclusions from plugins
943   *    - path (required)
944   *    - version (optional) used to force a browser refresh
945   *    - order (optional)
946   *    - template (optional) set to true to allow smarty syntax in the css file
947   */
[17021]948  function func_combine_css($params)
[7987]949  {
[25506]950    if (empty($params['path']))
951    {
952      fatal_error('combine_css missing path');
953    }
954
955    if (!isset($params['id']))
956    {
957      $params['id'] = md5($params['path']);
958    }
959
[25568]960    $this->cssLoader->add($params['id'], $params['path'], isset($params['version']) ? $params['version'] : 0, (int)@$params['order'], (bool)@$params['template']);
[7987]961  }
[7975]962
[25812]963  /**
964   * The "get_combined_scripts" function returns a placeholder for delayed
965   * CSS files combination and minification.
966   *
967   * @param array $params (unused)
968   */
[17021]969  function func_get_combined_css($params)
[7987]970  {
[23384]971    return self::COMBINED_CSS_TAG;
[7987]972  }
973
[25812]974  /**
975   * Declares a Smarty prefilter from a plugin, allowing it to modify template
976   * source before compilation and without changing core files.
[3927]977   * They will be processed by weight ascending.
[25812]978   * @see http://www.smarty.net/manual/en/advanced.features.prefilters.php
979   *
980   * @param string $handle
981   * @param Callable $callback
982   * @param int $weight
[3927]983   */
[3951]984  function set_prefilter($handle, $callback, $weight=50)
[3927]985  {
[23495]986    $this->external_filters[$handle][$weight][] = array('pre', $callback);
[3928]987    ksort($this->external_filters[$handle]);
[3927]988  }
[3951]989
[25812]990  /**
991   * Declares a Smarty postfilter.
992   * They will be processed by weight ascending.
993   * @see http://www.smarty.net/manual/en/advanced.features.postfilters.php
994   *
995   * @param string $handle
996   * @param Callable $callback
997   * @param int $weight
998   */
[3951]999  function set_postfilter($handle, $callback, $weight=50)
1000  {
[23495]1001    $this->external_filters[$handle][$weight][] = array('post', $callback);
[3951]1002    ksort($this->external_filters[$handle]);
1003  }
1004
[25812]1005  /**
1006   * Declares a Smarty outputfilter.
1007   * They will be processed by weight ascending.
1008   * @see http://www.smarty.net/manual/en/advanced.features.outputfilters.php
1009   *
1010   * @param string $handle
1011   * @param Callable $callback
1012   * @param int $weight
1013   */
[3951]1014  function set_outputfilter($handle, $callback, $weight=50)
1015  {
[23495]1016    $this->external_filters[$handle][$weight][] = array('output', $callback);
[3951]1017    ksort($this->external_filters[$handle]);
1018  }
[5177]1019
[25812]1020  /**
1021   * Register the filters for the tpl file.
1022   *
1023   * @param string $handle
[3927]1024   */
[3928]1025  function load_external_filters($handle)
[3927]1026  {
[3928]1027    if (isset($this->external_filters[$handle]))
[3927]1028    {
[3951]1029      $compile_id = '';
[5177]1030      foreach ($this->external_filters[$handle] as $filters)
[3928]1031      {
[5177]1032        foreach ($filters as $filter)
[3928]1033        {
[3951]1034          list($type, $callback) = $filter;
1035          $compile_id .= $type.( is_array($callback) ? implode('', $callback) : $callback );
[23476]1036          $this->smarty->registerFilter($type, $callback);
[3928]1037        }
1038      }
[3951]1039      $this->smarty->compile_id .= '.'.base_convert(crc32($compile_id), 10, 36);
[3927]1040    }
1041  }
[3928]1042
[25812]1043  /**
1044   * Unregister the filters for the tpl file.
1045   *
1046   * @param string $handle
1047   */
[3928]1048  function unload_external_filters($handle)
1049  {
1050    if (isset($this->external_filters[$handle]))
1051    {
[5177]1052      foreach ($this->external_filters[$handle] as $filters)
[3928]1053      {
[5177]1054        foreach ($filters as $filter)
[3928]1055        {
[3951]1056          list($type, $callback) = $filter;
[23476]1057          $this->smarty->unregisterFilter($type, $callback);
[3928]1058        }
1059      }
1060    }
1061  }
1062
[25812]1063  /**
1064   * @toto : description of Template::prefilter_white_space
1065   *
1066   * @param string $source
1067   * @param Smarty $smarty
1068   * @param return string
1069   */
[23384]1070  static function prefilter_white_space($source, $smarty)
[2481]1071  {
1072    $ld = $smarty->left_delimiter;
1073    $rd = $smarty->right_delimiter;
1074    $ldq = preg_quote($ld, '#');
1075    $rdq = preg_quote($rd, '#');
1076
1077    $regex = array();
[8378]1078    $tags = array('if','foreach','section','footer_script');
[2481]1079    foreach($tags as $tag)
1080    {
[23588]1081      $regex[] = "#^[ \t]+($ldq$tag"."[^$ld$rd]*$rdq)\s*$#m";
1082      $regex[] = "#^[ \t]+($ldq/$tag$rdq)\s*$#m";
[2481]1083    }
[8378]1084    $tags = array('include','else','combine_script','html_head');
[2481]1085    foreach($tags as $tag)
1086    {
[23588]1087      $regex[] = "#^[ \t]+($ldq$tag"."[^$ld$rd]*$rdq)\s*$#m";
[2481]1088    }
1089    $source = preg_replace( $regex, "$1", $source);
1090    return $source;
1091  }
1092
[2334]1093  /**
[25812]1094   * Postfilter used when $conf['compiled_template_cache_language'] is true.
1095   *
1096   * @param string $source
1097   * @param Smarty $smarty
1098   * @param return string
[2334]1099   */
[23688]1100  static function postfilter_language($source, $smarty)
[2334]1101  {
[23688]1102    // replaces echo PHP_STRING_LITERAL; with the string literal value
1103    $source = preg_replace_callback(
1104      '/\\<\\?php echo ((?:\'(?:(?:\\\\.)|[^\'])*\')|(?:"(?:(?:\\\\.)|[^"])*"));\\?\\>\\n/',
1105      create_function('$matches', 'eval(\'$tmp=\'.$matches[1].\';\');return $tmp;'),
1106      $source);
[2334]1107    return $source;
1108  }
[5208]1109
[25812]1110  /**
1111   * Prefilter used to add theme local CSS files.
1112   *
1113   * @param string $source
1114   * @param Smarty $smarty
1115   * @param return string
1116   */
[23384]1117  static function prefilter_local_css($source, $smarty)
[5208]1118  {
1119    $css = array();
[23384]1120    foreach ($smarty->getTemplateVars('themes') as $theme)
[5208]1121    {
[8722]1122      $f = PWG_LOCAL_DIR.'css/'.$theme['id'].'-rules.css';
[7987]1123      if (file_exists(PHPWG_ROOT_PATH.$f))
[5208]1124      {
[23588]1125        $css[] = "{combine_css path='$f' order=10}";
[5208]1126      }
1127    }
[8722]1128    $f = PWG_LOCAL_DIR.'css/rules.css';
[7987]1129    if (file_exists(PHPWG_ROOT_PATH.$f))
[5208]1130    {
[23588]1131      $css[] = "{combine_css path='$f' order=10}";
[5208]1132    }
1133
1134    if (!empty($css))
1135    {
[28600]1136      $source = str_replace("{get_combined_css}", implode( "\n", $css )."\n{get_combined_css}", $source);
[5208]1137    }
1138
1139    return $source;
1140  }
[5446]1141
[25812]1142  /**
1143   * Loads the configuration file from a theme directory and returns it.
1144   *
1145   * @param string $dir
1146   * @return array
1147   */
[5446]1148  function load_themeconf($dir)
1149  {
[5448]1150    global $themeconfs, $conf;
[5446]1151
1152    $dir = realpath($dir);
1153    if (!isset($themeconfs[$dir]))
1154    {
1155      $themeconf = array();
1156      include($dir.'/themeconf.inc.php');
1157      // Put themeconf in cache
1158      $themeconfs[$dir] = $themeconf;
1159    }
1160    return $themeconfs[$dir];
1161  }
[21818]1162
[25812]1163  /**
1164   * Registers a button to be displayed on picture page.
1165   *
1166   * @param string $content
1167   * @param int $rank
1168   */
[23263]1169  function add_picture_button($content, $rank=BUTTONS_RANK_NEUTRAL)
[18760]1170  {
1171    $this->picture_buttons[$rank][] = $content;
1172  }
[21818]1173
[25812]1174  /**
1175   * Registers a button to be displayed on index pages.
1176   *
1177   * @param string $content
1178   * @param int $rank
1179   */
[23263]1180  function add_index_button($content, $rank=BUTTONS_RANK_NEUTRAL)
[18760]1181  {
1182    $this->index_buttons[$rank][] = $content;
1183  }
[21818]1184
[25812]1185  /**
1186   * Assigns PLUGIN_PICTURE_BUTTONS template variable with registered picture buttons.
1187   */
[18760]1188  function parse_picture_buttons()
1189  {
1190    if (!empty($this->picture_buttons))
1191    {
1192      ksort($this->picture_buttons);
[26863]1193      $buttons = array();
1194      foreach ($this->picture_buttons as $k => $row)
1195      {
1196        $buttons = array_merge($buttons, $row);
1197      }
1198      $this->assign('PLUGIN_PICTURE_BUTTONS', $buttons);
[28600]1199
[26863]1200      // only for PHP 5.3
1201      // $this->assign('PLUGIN_PICTURE_BUTTONS',
1202          // array_reduce(
1203            // $this->picture_buttons,
1204            // create_function('$v,$w', 'return array_merge($v, $w);'),
1205            // array()
1206          // ));
[18760]1207    }
1208  }
[21818]1209
[25812]1210  /**
1211   * Assigns PLUGIN_INDEX_BUTTONS template variable with registered index buttons.
1212   */
[18760]1213  function parse_index_buttons()
1214  {
1215    if (!empty($this->index_buttons))
1216    {
1217      ksort($this->index_buttons);
[26863]1218      $buttons = array();
1219      foreach ($this->index_buttons as $k => $row)
1220      {
1221        $buttons = array_merge($buttons, $row);
1222      }
1223      $this->assign('PLUGIN_INDEX_BUTTONS', $buttons);
[28600]1224
[26863]1225      // only for PHP 5.3
1226      // $this->assign('PLUGIN_INDEX_BUTTONS',
1227          // array_reduce(
1228            // $this->index_buttons,
1229            // create_function('$v,$w', 'return array_merge($v, $w);'),
1230            // array()
1231          // ));
[18760]1232    }
1233  }
[2216]1234}
1235
[3927]1236
[2216]1237/**
1238 * This class contains basic functions that can be called directly from the
1239 * templates in the form $pwg->l10n('edit')
1240 */
1241class PwgTemplateAdapter
1242{
[25815]1243  /**
1244   * @deprecated use "translate" modifier
1245   */
[2216]1246  function l10n($text)
1247  {
1248    return l10n($text);
1249  }
1250
[25815]1251  /**
1252   * @deprecated use "translate_dec" modifier
1253   */
[2216]1254  function l10n_dec($s, $p, $v)
1255  {
1256    return l10n_dec($s, $p, $v);
1257  }
1258
[25815]1259  /**
1260   * @deprecated use "translate" or "sprintf" modifier
1261   */
[2216]1262  function sprintf()
1263  {
1264    $args = func_get_args();
1265    return call_user_func_array('sprintf',  $args );
1266  }
[12920]1267
[25815]1268  /**
1269   * @param string $type
1270   * @param array $img
1271   * @return DerivativeImage
1272   */
[13170]1273  function derivative($type, $img)
1274  {
1275    return new DerivativeImage($type, $img);
1276  }
1277
[25815]1278  /**
1279   * @param string $type
1280   * @param array $img
1281   * @return string
1282   */
[12908]1283  function derivative_url($type, $img)
1284  {
1285    return DerivativeImage::url($type, $img);
1286  }
[2216]1287}
1288
[7975]1289
[25815]1290/**
1291 * A Combinable represents a JS or CSS file ready for cobination and minification.
1292 */
[25462]1293class Combinable
[7975]1294{
[25815]1295  /** @var string */
[8008]1296  public $id;
[25815]1297  /** @var string */
[7975]1298  public $path;
[25815]1299  /** @var string */
[7975]1300  public $version;
[25815]1301  /** @var bool */
[25462]1302  public $is_template;
[7975]1303
[25815]1304  /**
1305   * @param string $id
1306   * @param string $path
1307   * @param string $version
1308   */
1309  function __construct($id, $path, $version=0)
[7975]1310  {
[8008]1311    $this->id = $id;
[8012]1312    $this->set_path($path);
1313    $this->version = $version;
[25815]1314    $this->is_template = false;
[7975]1315  }
1316
[25815]1317  /**
1318   * @param string $path
1319   */
[7975]1320  function set_path($path)
1321  {
1322    if (!empty($path))
1323      $this->path = $path;
1324  }
[8012]1325
[25815]1326  /**
1327   * @return bool
1328   */
[7995]1329  function is_remote()
1330  {
[25815]1331    return url_is_remote($this->path) || strncmp($this->path, '//', 2)==0;
[7995]1332  }
[7975]1333}
1334
[25815]1335/**
1336 * Implementation of Combinable for JS files.
1337 */
[25462]1338final class Script extends Combinable
1339{
[25815]1340  /** @var int 0,1,2 */
[25462]1341  public $load_mode;
[25815]1342  /** @var array */
1343  public $precedents;
1344  /** @var array */
1345  public $extra;
[7975]1346
[25815]1347  /**
1348   * @param int 0,1,2
1349   * @param string $id
1350   * @param string $path
1351   * @param string $version
1352   * @param array $precedents
1353   */
1354  function __construct($load_mode, $id, $path, $version=0, $precedents=array())
[25462]1355  {
1356    parent::__construct($id, $path, $version);
1357    $this->load_mode = $load_mode;
1358    $this->precedents = $precedents;
[25815]1359    $this->extra = array();
[25462]1360  }
1361}
1362
[25815]1363/**
1364 * Implementation of Combinable for CSS files.
1365 */
[25462]1366final class Css extends Combinable
1367{
[25815]1368  /** @var int */
[25462]1369  public $order;
1370
[25815]1371  /**
1372   * @param string $id
1373   * @param string $path
1374   * @param string $version
1375   * @param int $order
1376   */
1377  function __construct($id, $path, $version=0, $order=0)
[25462]1378  {
1379    parent::__construct($id, $path, $version);
1380    $this->order = $order;
1381  }
1382}
1383
1384
[25815]1385/**
1386 * Manages a list of CSS files and combining them in a unique file.
1387 */
[25506]1388class CssLoader
1389{
[25815]1390  /** @param Css[] */
[25506]1391  private $registered_css;
[25815]1392  /** @param int used to keep declaration order */
[25506]1393  private $counter;
[28600]1394
[25506]1395  function __construct()
1396  {
1397    $this->clear();
1398  }
[28600]1399
[25506]1400  function clear()
1401  {
1402    $this->registered_css = array();
1403    $this->counter = 0;
1404  }
[28600]1405
[25815]1406  /**
1407   * @return Combinable[] array of combined CSS.
1408   */
[25506]1409  function get_css()
1410  {
1411    uasort($this->registered_css, array('CssLoader', 'cmp_by_order'));
[25544]1412    $combiner = new FileCombiner('css', $this->registered_css);
1413    return $combiner->combine();
[25506]1414  }
[28600]1415
[25815]1416  /**
1417   * Callback for CSS files sorting.
1418   */
[25506]1419  private static function cmp_by_order($a, $b)
1420  {
1421    return $a->order - $b->order;
1422  }
[28600]1423
[25815]1424  /**
1425   * Adds a new file, if a file with the same $id already exsists, the one with
1426   * the higher $order or higher $version is kept.
1427   *
1428   * @param string $id
1429   * @param string $path
1430   * @param string $version
1431   * @param int $order
1432   * @param bool $is_template
1433   */
[25506]1434  function add($id, $path, $version=0, $order=0, $is_template=false)
1435  {
1436    if (!isset($this->registered_css[$id]))
1437    {
1438      // costum order as an higher impact than declaration order
1439      $css = new Css($id, $path, $version, $order*1000+$this->counter);
1440      $css->is_template = $is_template;
1441      $this->registered_css[$id] = $css;
1442      $this->counter++;
1443    }
1444    else
1445    {
1446      $css = $this->registered_css[$id];
[26073]1447      if ($css->order<$order*1000 || version_compare($css->version, $version)<0)
[25506]1448      {
1449        unset($this->registered_css[$id]);
1450        $this->add($id, $path, $version, $order, $is_template);
1451      }
1452    }
1453  }
1454}
1455
1456
[25815]1457/**
1458 * Manage a list of required scripts for a page, by optimizing their loading location (head, footer, async)
1459 * and later on by combining them in a unique file respecting at the same time dependencies.
1460 */
[7975]1461class ScriptLoader
1462{
[25815]1463  /** @var Script[] */
[7975]1464  private $registered_scripts;
[25815]1465  /** @var string[] */
[7995]1466  public $inline_scripts;
1467
[25815]1468  /** @var bool */
[7975]1469  private $did_head;
[25815]1470  /** @var bool */
[7995]1471  private $head_done_scripts;
[25815]1472  /** @var bool */
[10616]1473  private $did_footer;
[7995]1474
[7975]1475  private static $known_paths = array(
1476      'core.scripts' => 'themes/default/js/scripts.js',
1477      'jquery' => 'themes/default/js/jquery.min.js',
[9559]1478      'jquery.ui' => 'themes/default/js/ui/minified/jquery.ui.core.min.js',
[18630]1479      'jquery.ui.effect' => 'themes/default/js/ui/minified/jquery.ui.effect.min.js',
[7975]1480    );
1481
[9559]1482  private static $ui_core_dependencies = array(
1483      'jquery.ui.widget' => array('jquery'),
1484      'jquery.ui.position' => array('jquery'),
1485      'jquery.ui.mouse' => array('jquery', 'jquery.ui', 'jquery.ui.widget'),
1486    );
1487
[7975]1488  function __construct()
1489  {
1490    $this->clear();
1491  }
1492
1493  function clear()
1494  {
1495    $this->registered_scripts = array();
[7995]1496    $this->inline_scripts = array();
1497    $this->head_done_scripts = array();
[10616]1498    $this->did_head = $this->did_footer = false;
[7975]1499  }
1500
[25815]1501  /**
1502   * @return bool
1503   */
1504  function did_head()
1505  {
1506    return $this->did_head;
1507  }
1508
1509  /**
1510   * @return Script[]
1511   */
[8506]1512  function get_all()
1513  {
1514    return $this->registered_scripts;
1515  }
1516
[25815]1517  /**
1518   * @param string $code
1519   * @param string[] $require
1520   */
[7995]1521  function add_inline($code, $require)
1522  {
[10616]1523    !$this->did_footer || trigger_error("Attempt to add inline script but the footer has been written", E_USER_WARNING);
[7995]1524    if(!empty($require))
1525    {
[9580]1526      foreach ($require as $id)
1527      {
1528        if(!isset($this->registered_scripts[$id]))
1529          $this->load_known_required_script($id, 1) or fatal_error("inline script not found require $id");
1530        $s = $this->registered_scripts[$id];
1531        if($s->load_mode==2)
1532          $s->load_mode=1; // until now the implementation does not allow executing inline script depending on another async script
1533      }
[7995]1534    }
1535    $this->inline_scripts[] = $code;
1536  }
1537
[25815]1538  /**
1539   * @param string $id
1540   * @param int $load_mode
1541   * @param string[] $require
1542   * @param string $path
1543   * @param string $version
1544   */
[26718]1545  function add($id, $load_mode, $require, $path, $version=0, $is_template=false)
[7975]1546  {
[10616]1547    if ($this->did_head && $load_mode==0)
[7975]1548    {
[10616]1549      trigger_error("Attempt to add script $id but the head has been written", E_USER_WARNING);
[7975]1550    }
[10616]1551    elseif ($this->did_footer)
1552    {
1553      trigger_error("Attempt to add script $id but the footer has been written", E_USER_WARNING);
1554    }
[7975]1555    if (! isset( $this->registered_scripts[$id] ) )
1556    {
[8012]1557      $script = new Script($load_mode, $id, $path, $version, $require);
[26718]1558      $script->is_template = $is_template;
[7975]1559      self::fill_well_known($id, $script);
1560      $this->registered_scripts[$id] = $script;
[10616]1561
1562      // Load or modify all UI core files
1563      if ($id == 'jquery.ui' and $script->path == self::$known_paths['jquery.ui'])
1564      {
1565        foreach (self::$ui_core_dependencies as $script_id => $required_ids)
1566          $this->add($script_id, $load_mode, $required_ids, null, $version);
1567      }
1568
1569      // Try to load undefined required script
1570      foreach ($script->precedents as $script_id)
1571      {
1572        if (! isset( $this->registered_scripts[$script_id] ) )
1573          $this->load_known_required_script($script_id, $load_mode);
1574      }
[7975]1575    }
1576    else
1577    {
[10616]1578      $script = $this->registered_scripts[$id];
[7975]1579      if (count($require))
1580      {
1581        $script->precedents = array_unique( array_merge($script->precedents, $require) );
1582      }
1583      $script->set_path($path);
1584      if ($version && version_compare($script->version, $version)<0 )
1585        $script->version = $version;
1586      if ($load_mode < $script->load_mode)
1587        $script->load_mode = $load_mode;
1588    }
1589  }
1590
[25815]1591  /**
1592   * Returns combined scripts loaded in header.
1593   *
1594   * @return Combinable[]
1595   */
[7975]1596  function get_head_scripts()
1597  {
[7995]1598    self::check_load_dep($this->registered_scripts);
[7975]1599    foreach( array_keys($this->registered_scripts) as $id )
1600    {
1601      $this->compute_script_topological_order($id);
1602    }
1603
1604    uasort($this->registered_scripts, array('ScriptLoader', 'cmp_by_mode_and_order'));
1605
1606    foreach( $this->registered_scripts as $id => $script)
1607    {
1608      if ($script->load_mode > 0)
1609        break;
1610      if ( !empty($script->path) )
[7995]1611        $this->head_done_scripts[$id] = $script;
[7975]1612      else
1613        trigger_error("Script $id has an undefined path", E_USER_WARNING);
1614    }
1615    $this->did_head = true;
[8012]1616    return self::do_combine($this->head_done_scripts, 0);
[7975]1617  }
1618
[25815]1619  /**
1620   * Returns combined scripts loaded in footer.
1621   *
1622   * @return Combinable[]
1623   */
[7975]1624  function get_footer_scripts()
1625  {
[10656]1626    if (!$this->did_head)
1627    {
1628      self::check_load_dep($this->registered_scripts);
1629    }
[10616]1630    $this->did_footer = true;
[7995]1631    $todo = array();
[7975]1632    foreach( $this->registered_scripts as $id => $script)
1633    {
[7995]1634      if (!isset($this->head_done_scripts[$id]))
[7975]1635      {
[7995]1636        $todo[$id] = $script;
1637      }
1638    }
[8401]1639
[7995]1640    foreach( array_keys($todo) as $id )
1641    {
1642      $this->compute_script_topological_order($id);
1643    }
1644
1645    uasort($todo, array('ScriptLoader', 'cmp_by_mode_and_order'));
1646
1647    $result = array( array(), array() );
1648    foreach( $todo as $id => $script)
1649    {
1650      $result[$script->load_mode-1][$id] = $script;
1651    }
[8012]1652    return array( self::do_combine($result[0],1), self::do_combine($result[1],2) );
[7995]1653  }
1654
[25815]1655  /**
1656   * @param Script[] $scripts
1657   * @param int $load_mode
1658   * @return Combinable[]
1659   */
[8170]1660  private static function do_combine($scripts, $load_mode)
1661  {
[25544]1662    $combiner = new FileCombiner('js', $scripts);
[25462]1663    return $combiner->combine();
[8170]1664  }
[8401]1665
[25815]1666  /**
1667   * Checks dependencies among Scripts.
1668   * Checks that if B depends on A, then B->load_mode >= A->load_mode in order to respect execution order.
1669   *
1670   * @param Script[] $scripts
1671   */
[7995]1672  private static function check_load_dep($scripts)
1673  {
[8170]1674    global $conf;
[7995]1675    do
1676    {
1677      $changed = false;
1678      foreach( $scripts as $id => $script)
1679      {
1680        $load = $script->load_mode;
1681        foreach( $script->precedents as $precedent)
[7975]1682        {
[7995]1683          if ( !isset($scripts[$precedent] ) )
1684            continue;
1685          if ( $scripts[$precedent]->load_mode > $load )
1686          {
1687            $scripts[$precedent]->load_mode = $load;
1688            $changed = true;
1689          }
[8401]1690          if ($load==2 && $scripts[$precedent]->load_mode==2 && ($scripts[$precedent]->is_remote() or !$conf['template_combine_files']) )
[7995]1691          {// we are async -> a predecessor cannot be async unlesss it can be merged; otherwise script execution order is not guaranteed
1692            $scripts[$precedent]->load_mode = 1;
1693            $changed = true;
1694          }
[7975]1695        }
1696      }
1697    }
[7995]1698    while ($changed);
[7975]1699  }
[8012]1700
[25815]1701  /**
1702   * Fill a script dependancies with the known jQuery UI scripts.
1703   *
1704   * @param string $id in FileCombiner::$known_paths
1705   * @param Script $script
1706   */
[7995]1707  private static function fill_well_known($id, $script)
1708  {
1709    if ( empty($script->path) && isset(self::$known_paths[$id]))
1710    {
1711      $script->path = self::$known_paths[$id];
1712    }
1713    if ( strncmp($id, 'jquery.', 7)==0 )
1714    {
[9559]1715      $required_ids = array('jquery');
1716
[18630]1717      if ( strncmp($id, 'jquery.ui.effect-', 17)==0 )
[9559]1718      {
[18630]1719        $required_ids = array('jquery', 'jquery.ui.effect');
[9559]1720
1721        if ( empty($script->path) )
[18630]1722          $script->path = dirname(self::$known_paths['jquery.ui.effect'])."/$id.min.js";
[9559]1723      }
[18630]1724      elseif ( strncmp($id, 'jquery.ui.', 10)==0 )
[9559]1725      {
[18630]1726        if ( !isset(self::$ui_core_dependencies[$id]) )
1727          $required_ids = array_merge(array('jquery', 'jquery.ui'), array_keys(self::$ui_core_dependencies));
[9559]1728
1729        if ( empty($script->path) )
[18630]1730          $script->path = dirname(self::$known_paths['jquery.ui'])."/$id.min.js";
[9559]1731      }
1732
1733      foreach ($required_ids as $required_id)
1734      {
1735        if ( !in_array($required_id, $script->precedents ) )
1736          $script->precedents[] = $required_id;
1737      }
[7995]1738    }
1739  }
[7975]1740
[25815]1741  /**
1742   * Add a known jQuery UI script to loaded scripts.
1743   *
1744   * @param string $id in FileCombiner::$known_paths
1745   * @param int $load_mode
1746   * @return bool
1747   */
[9580]1748  private function load_known_required_script($id, $load_mode)
1749  {
[18630]1750    if ( isset(self::$known_paths[$id]) or strncmp($id, 'jquery.ui.', 10)==0  )
[9580]1751    {
1752      $this->add($id, $load_mode, array(), null);
1753      return true;
1754    }
1755    return false;
1756  }
1757
[25815]1758  /**
1759   * Compute script order depending on dependencies.
1760   * Assigned to $script->extra['order'].
1761   *
1762   * @param string $script_id
1763   * @param int $recursion_limiter
1764   * @return int
1765   */
[8170]1766  private function compute_script_topological_order($script_id, $recursion_limiter=0)
[7975]1767  {
1768    if (!isset($this->registered_scripts[$script_id]))
1769    {
1770      trigger_error("Undefined script $script_id is required by someone", E_USER_WARNING);
1771      return 0;
1772    }
[8170]1773    $recursion_limiter<5 or fatal_error("combined script circular dependency");
[10616]1774    $script = $this->registered_scripts[$script_id];
[7975]1775    if (isset($script->extra['order']))
1776      return $script->extra['order'];
1777    if (count($script->precedents) == 0)
1778      return ($script->extra['order'] = 0);
1779    $max = 0;
1780    foreach( $script->precedents as $precedent)
[8170]1781      $max = max($max, $this->compute_script_topological_order($precedent, $recursion_limiter+1) );
[7975]1782    $max++;
1783    return ($script->extra['order'] = $max);
1784  }
1785
[25815]1786  /**
1787   * Callback for scripts sorter.
1788   */
[7975]1789  private static function cmp_by_mode_and_order($s1, $s2)
1790  {
1791    $ret = $s1->load_mode - $s2->load_mode;
[7995]1792    if ($ret) return $ret;
[8012]1793
[7995]1794    $ret = $s1->extra['order'] - $s2->extra['order'];
1795    if ($ret) return $ret;
[8012]1796
[7995]1797    if ($s1->extra['order']==0 and ($s1->is_remote() xor $s2->is_remote()) )
1798    {
1799      return $s1->is_remote() ? -1 : 1;
1800    }
1801    return strcmp($s1->id,$s2->id);
[7975]1802  }
1803}
1804
[7987]1805
[25815]1806/**
1807 * Allows merging of javascript and css files into a single one.
1808 */
[7987]1809final class FileCombiner
1810{
[25815]1811  /** @var string 'js' or 'css' */
1812  private $type;
1813  /** @var bool */
[25462]1814  private $is_css;
[25815]1815  /** @var Combinable[] */
[25544]1816  private $combinables;
[7987]1817
[25815]1818  /**
1819   * @param string $type 'js' or 'css'
1820   * @param Combinable[] $combinables
1821   */
1822  function __construct($type, $combinables=array())
[7987]1823  {
1824    $this->type = $type;
[25462]1825    $this->is_css = $type=='css';
[25544]1826    $this->combinables = $combinables;
[7987]1827  }
[8012]1828
[25815]1829  /**
1830   * Deletes all combined files from cache directory.
1831   */
[7995]1832  static function clear_combined_files()
1833  {
[12802]1834    $dir = opendir(PHPWG_ROOT_PATH.PWG_COMBINED_DIR);
[7995]1835    while ($file = readdir($dir))
1836    {
1837      if ( get_extension($file)=='js' || get_extension($file)=='css')
[12802]1838        unlink(PHPWG_ROOT_PATH.PWG_COMBINED_DIR.$file);
[7995]1839    }
1840    closedir($dir);
1841  }
[7987]1842
[25815]1843  /**
1844   * @param Combinable|Combinable[] $combinable
1845   */
1846  function add($combinable)
[7987]1847  {
[25815]1848    if (is_array($combinable))
[25547]1849    {
[25815]1850      $this->combinables = array_merge($this->combinables, $combinable);
[25547]1851    }
1852    else
1853    {
[25815]1854      $this->combinables[] = $combinable;
[25547]1855    }
[7987]1856  }
1857
[25815]1858  /**
1859   * @return Combinable[]
1860   */
[25462]1861  function combine()
[7987]1862  {
[25462]1863    global $conf;
1864    $force = false;
1865    if (is_admin() && ($this->is_css || !$conf['template_compile_check']) )
1866    {
1867      $force = (isset($_SERVER['HTTP_CACHE_CONTROL']) && strpos($_SERVER['HTTP_CACHE_CONTROL'], 'max-age=0') !== false)
1868        || (isset($_SERVER['HTTP_PRAGMA']) && strpos($_SERVER['HTTP_PRAGMA'], 'no-cache'));
1869    }
1870
1871    $result = array();
1872    $pending = array();
[25544]1873    $ini_key = $this->is_css ? array(get_absolute_root_url(false)): array(); //because for css we modify bg url;
1874    $key = $ini_key;
[25462]1875
1876    foreach ($this->combinables as $combinable)
1877    {
[25544]1878      if ($combinable->is_remote())
[25462]1879      {
[25544]1880        $this->flush_pending($result, $pending, $key, $force);
1881        $key = $ini_key;
1882        $result[] = $combinable;
1883        continue;
[25462]1884      }
[25544]1885      elseif (!$conf['template_combine_files'])
[25462]1886      {
1887        $this->flush_pending($result, $pending, $key, $force);
[25544]1888        $key = $ini_key;
[25462]1889      }
[25544]1890
1891      $key[] = $combinable->path;
1892      $key[] = $combinable->version;
1893      if ($conf['template_compile_check'])
1894        $key[] = filemtime( PHPWG_ROOT_PATH . $combinable->path );
1895      $pending[] = $combinable;
[25462]1896    }
1897    $this->flush_pending($result, $pending, $key, $force);
1898    return $result;
[7987]1899  }
1900
[25815]1901  /**
1902   * Process a set of pending files.
1903   *
1904   * @param array &$result
1905   * @param array &$pending
1906   * @param string[] $key
1907   * @param bool $force
1908   */
[25568]1909  private function flush_pending(&$result, &$pending, $key, $force)
[7987]1910  {
[25462]1911    if (count($pending)>1)
[7987]1912    {
[25462]1913      $key = join('>', $key);
1914      $file = PWG_COMBINED_DIR . base_convert(crc32($key),10,36) . '.' . $this->type;
1915      if ($force || !file_exists(PHPWG_ROOT_PATH.$file) )
1916      {
1917        $output = '';
[29636]1918        $header = '';
[25462]1919        foreach ($pending as $combinable)
1920        {
1921          $output .= "/*BEGIN $combinable->path */\n";
[29636]1922          $output .= $this->process_combinable($combinable, true, $force, $header);
[25462]1923          $output .= "\n";
1924        }
[29636]1925        $output = "/*BEGIN header */\n" . $header . "\n" . $output;
[25462]1926        mkgetdir( dirname(PHPWG_ROOT_PATH.$file) );
1927        file_put_contents( PHPWG_ROOT_PATH.$file, $output );
1928        @chmod(PHPWG_ROOT_PATH.$file, 0644);
1929      }
1930      $result[] = new Combinable("combi", $file, false);
[7987]1931    }
[25462]1932    elseif ( count($pending)==1)
[7987]1933    {
[29636]1934      $header = '';
1935      $this->process_combinable($pending[0], false, $force, $header);
[25462]1936      $result[] = $pending[0];
[7987]1937    }
1938    $key = array();
[25462]1939    $pending = array();
1940  }
[7987]1941
[25815]1942  /**
1943   * Process one combinable file.
1944   *
1945   * @param Combinable $combinable
1946   * @param bool $return_content
1947   * @param bool $force
[29636]1948   * @param string $header CSS directives that must appear first in
1949   *                       the minified file (only used when
1950   *                       $return_content===true)
[25815]1951   * @return null|string
1952   */
[29636]1953  private function process_combinable($combinable, $return_content, $force, &$header)
[25462]1954  {
[25464]1955    global $conf;
[25462]1956    if ($combinable->is_template)
[7987]1957    {
[25462]1958      if (!$return_content)
1959      {
1960        $key = array($combinable->path, $combinable->version);
1961        if ($conf['template_compile_check'])
1962          $key[] = filemtime( PHPWG_ROOT_PATH . $combinable->path );
[25464]1963        $file = PWG_COMBINED_DIR . 't' . base_convert(crc32(implode(',',$key)),10,36) . '.' . $this->type;
[25462]1964        if (!$force && file_exists(PHPWG_ROOT_PATH.$file) )
1965        {
1966          $combinable->path = $file;
1967          $combinable->version = false;
1968          return;
1969        }
[8075]1970      }
1971
[25462]1972      global $template;
1973      $handle = $this->type. '.' .$combinable->id;
1974      $template->set_filename($handle, realpath(PHPWG_ROOT_PATH.$combinable->path));
[28587]1975      trigger_notify( 'combinable_preparse', $template, $combinable, $this); //allow themes and plugins to set their own vars to template ...
[25462]1976      $content = $template->parse($handle, true);
1977
1978      if ($this->is_css)
[29636]1979        $content = self::process_css($content, $combinable->path, $header );
[25462]1980      else
1981        $content = self::process_js($content, $combinable->path );
1982
1983      if ($return_content)
1984        return $content;
1985      file_put_contents( PHPWG_ROOT_PATH.$file, $content );
1986      $combinable->path = $file;
[7987]1987    }
[25462]1988    elseif ($return_content)
[7987]1989    {
[25462]1990      $content = file_get_contents(PHPWG_ROOT_PATH . $combinable->path);
1991      if ($this->is_css)
[29636]1992        $content = self::process_css($content, $combinable->path, $header );
[7987]1993      else
[25462]1994        $content = self::process_js($content, $combinable->path );
1995      return $content;
[7987]1996    }
1997  }
1998
[25815]1999  /**
2000   * Process a JS file.
2001   *
2002   * @param string $js file content
2003   * @param string $file
2004   * @return string
2005   */
[25462]2006  private static function process_js($js, $file)
[8075]2007  {
2008    if (strpos($file, '.min')===false and strpos($file, '.packed')===false )
2009    {
[19576]2010      require_once(PHPWG_ROOT_PATH.'include/jshrink.class.php');
2011      try { $js = JShrink_Minifier::minify($js); } catch(Exception $e) {}
[8075]2012    }
[9606]2013    return trim($js, " \t\r\n;").";\n";
[8075]2014  }
[8401]2015
[25815]2016  /**
2017   * Process a CSS file.
2018   *
2019   * @param string $css file content
2020   * @param string $file
[29636]2021   * @param string $header CSS directives that must appear first in
2022   *                       the minified file.
[25815]2023   * @return string
2024   */
[29636]2025  private static function process_css($css, $file, &$header)
[7987]2026  {
[29636]2027    $css = self::process_css_rec($css, dirname($file), $header);
[25815]2028    if (strpos($file, '.min')===false and version_compare(PHP_VERSION, '5.2.4', '>='))
[16278]2029    {
2030      require_once(PHPWG_ROOT_PATH.'include/cssmin.class.php');
2031      $css = CssMin::minify($css, array('Variables'=>false));
2032    }
[28587]2033    $css = trigger_change('combined_css_postfilter', $css);
[8170]2034    return $css;
2035  }
[8401]2036
[25815]2037  /**
2038   * Resolves relative links in CSS file.
2039   *
2040   * @param string $css file content
2041   * @param string $dir
[29636]2042   * @param string $header CSS directives that must appear first in
2043   *                       the minified file.
[25815]2044   * @return string
2045   */
[29636]2046  private static function process_css_rec($css, $dir, &$header)
[8170]2047  {
[25800]2048    static $PATTERN_URL = "#url\(\s*['|\"]{0,1}(.*?)['|\"]{0,1}\s*\)#";
2049    static $PATTERN_IMPORT = "#@import\s*['|\"]{0,1}(.*?)['|\"]{0,1};#";
[25462]2050
[25800]2051    if (preg_match_all($PATTERN_URL, $css, $matches, PREG_SET_ORDER))
[7987]2052    {
2053      $search = $replace = array();
2054      foreach ($matches as $match)
2055      {
[25800]2056        if ( !url_is_remote($match[1]) && $match[1][0] != '/' && strpos($match[1], 'data:image/')===false)
[7987]2057        {
[25462]2058          $relative = $dir . "/$match[1]";
[7987]2059          $search[] = $match[0];
[8401]2060          $replace[] = 'url('.embellish_url(get_absolute_root_url(false).$relative).')';
[7987]2061        }
2062      }
2063      $css = str_replace($search, $replace, $css);
2064    }
2065
[25800]2066    if (preg_match_all($PATTERN_IMPORT, $css, $matches, PREG_SET_ORDER))
[7987]2067    {
2068      $search = $replace = array();
[29636]2069     
[7987]2070      foreach ($matches as $match)
2071      {
2072        $search[] = $match[0];
[29636]2073       
2074        if (
2075          strpos($match[1], '..') !== false // Possible attempt to get out of Piwigo's dir
2076          or strpos($match[1], '://') !== false // Remote URL
2077          or !is_readable(PHPWG_ROOT_PATH . $dir . '/' . $match[1])
2078          )
2079        {
2080          // If anything is suspicious, don't try to process the
2081          // @import. Since @import need to be first and we are
2082          // concatenating several CSS files, remove it from here and return
2083          // it through $header.
2084          $header .= $match[0];
2085          $replace[] = '';
2086        }
2087        else
2088        {
2089          $sub_css = file_get_contents(PHPWG_ROOT_PATH . $dir . "/$match[1]");
2090          $replace[] = self::process_css_rec($sub_css, dirname($dir . "/$match[1]"), $header);
2091        }
[7987]2092      }
2093      $css = str_replace($search, $replace, $css);
2094    }
2095    return $css;
2096  }
2097}
2098
[7995]2099?>
Note: See TracBrowser for help on using the repository browser.