source: tags/2.7.0RC2/include/template.class.php @ 30381

Last change on this file since 30381 was 29389, checked in by plg, 10 years ago

feature 3133: add colorscheme parameter for themes. This way we can use the
appropriate CSS for selectize (and other things)

  • Property svn:eol-style set to LF
File size: 55.7 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/**
25 * @package template
26 */
27
28require_once( PHPWG_ROOT_PATH .'include/smarty/libs/Smarty.class.php');
29
30
31/** default rank for buttons */
32define('BUTTONS_RANK_NEUTRAL', 50);
33
34/**
35 * This a wrapper arround Smarty classes proving various custom mechanisms for templates.
36 */
37class Template
38{
39  /** @var Smarty */
40  var $smarty;
41  /** @var string */
42  var $output = '';
43
44  /** @var string[] - Hash of filenames for each template handle. */
45  var $files = array();
46  /** @var string[] - Template extents filenames for each template handle. */
47  var $extents = array();
48  /** @var array - Templates prefilter from external sources (plugins) */
49  var $external_filters = array();
50
51  /** @var string - Content to add before </head> tag */
52  var $html_head_elements = array();
53  /** @var string - Runtime CSS rules */
54  private $html_style = '';
55
56  /** @const string */
57  const COMBINED_SCRIPTS_TAG = '<!-- COMBINED_SCRIPTS -->';
58  /** @var ScriptLoader */
59  var $scriptLoader;
60
61  /** @const string */
62  const COMBINED_CSS_TAG = '<!-- COMBINED_CSS -->';
63  /** @var CssLoader */
64  var $cssLoader;
65
66  /** @var array - Runtime buttons on picture page */
67  var $picture_buttons = array();
68  /** @var array - Runtime buttons on index page */
69  var $index_buttons = array();
70
71
72  /**
73   * @var string $root
74   * @var string $theme
75   * @var string $path
76   */
77  function __construct($root=".", $theme="", $path="template")
78  {
79    global $conf, $lang_info;
80
81    SmartyException::$escape = false;
82
83    $this->scriptLoader = new ScriptLoader;
84    $this->cssLoader = new CssLoader;
85    $this->smarty = new Smarty;
86    $this->smarty->debugging = $conf['debug_template'];
87    if (!$this->smarty->debugging)
88    {
89      $this->smarty->error_reporting = error_reporting() & ~E_NOTICE;
90    }
91    $this->smarty->compile_check = $conf['template_compile_check'];
92    $this->smarty->force_compile = $conf['template_force_compile'];
93
94    if (!isset($conf['data_dir_checked']))
95    {
96      $dir = PHPWG_ROOT_PATH.$conf['data_location'];
97      mkgetdir($dir, MKGETDIR_DEFAULT&~MKGETDIR_DIE_ON_ERROR);
98      if (!is_writable($dir))
99      {
100        load_language('admin.lang');
101        fatal_error(
102          l10n(
103            'Give write access (chmod 777) to "%s" directory at the root of your Piwigo installation',
104            $conf['data_location']
105            ),
106          l10n('an error happened'),
107          false // show trace
108          );
109      }
110      if (function_exists('pwg_query')) {
111        conf_update_param('data_dir_checked', 1);
112      }
113    }
114
115    $compile_dir = PHPWG_ROOT_PATH.$conf['data_location'].'templates_c';
116    mkgetdir( $compile_dir );
117
118    $this->smarty->setCompileDir($compile_dir);
119
120    $this->smarty->assign( 'pwg', new PwgTemplateAdapter() );
121    $this->smarty->registerPlugin('modifiercompiler', 'translate', array('Template', 'modcompiler_translate') );
122    $this->smarty->registerPlugin('modifiercompiler', 'translate_dec', array('Template', 'modcompiler_translate_dec') );
123    $this->smarty->registerPlugin('modifier', 'explode', array('Template', 'mod_explode') );
124    $this->smarty->registerPlugin('modifier', 'ternary', array('Template', 'mod_ternary') );
125    $this->smarty->registerPlugin('modifier', 'get_extent', array($this, 'get_extent') );
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') );
135    if ( $conf['compiled_template_cache_language'] )
136    {
137      $this->smarty->registerFilter('post', array('Template', 'postfilter_language') );
138    }
139
140    $this->smarty->setTemplateDir(array());
141    if ( !empty($theme) )
142    {
143      $this->set_theme($root, $theme, $path);
144      if (!defined('IN_ADMIN'))
145      {
146        $this->set_prefilter( 'header', array('Template', 'prefilter_local_css') );
147      }
148    }
149    else
150      $this->set_template_dir($root);
151
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   
162    $this->smarty->assign('lang_info', $lang_info);
163
164    if (!defined('IN_ADMIN') and isset($conf['extents_for_templates']))
165    {
166      $tpl_extents = unserialize($conf['extents_for_templates']);
167      $this->set_extents($tpl_extents, './template-extension/', true, $theme);
168    }
169  }
170
171  /**
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
179   */
180  function set_theme($root, $theme, $path, $load_css=true, $load_local_head=true, $colorscheme='dark')
181  {
182    $this->set_template_dir($root.'/'.$theme.'/'.$path);
183
184    $themeconf = $this->load_themeconf($root.'/'.$theme);
185
186    if (isset($themeconf['parent']) and $themeconf['parent'] != $theme)
187    {
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      );
195    }
196
197    $tpl_var = array(
198      'id' => $theme,
199      'load_css' => $load_css,
200    );
201    if (!empty($themeconf['local_head']) and $load_local_head)
202    {
203      $tpl_var['local_head'] = realpath($root.'/'.$theme.'/'.$themeconf['local_head'] );
204    }
205    $themeconf['id'] = $theme;
206
207    if (!isset($themeconf['colorscheme']))
208    {
209      $themeconf['colorscheme'] = $colorscheme;
210    }
211   
212    $this->smarty->append('themes', $tpl_var);
213    $this->smarty->append('themeconf', $themeconf, true);
214  }
215
216  /**
217   * Adds template directory for this Template object.
218   * Also set compile id if not exists.
219   *
220   * @param string $dir
221   */
222  function set_template_dir($dir)
223  {
224    $this->smarty->addTemplateDir($dir);
225
226    if (!isset($this->smarty->compile_id))
227    {
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 );
231    }
232  }
233
234  /**
235   * Gets the template root directory for this Template object.
236   *
237   * @return string
238   */
239  function get_template_dir()
240  {
241    return $this->smarty->getTemplateDir();
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;
251      $this->smarty->clearCompiledTemplate();
252      $this->smarty->compile_id = $save_compile_id;
253      file_put_contents($this->smarty->getCompileDir().'/index.htm', 'Not allowed!');
254  }
255
256  /**
257   * Returns theme's parameter.
258   *
259   * @param string $val
260   * @return mixed
261   */
262  function get_themeconf($val)
263  {
264    $tc = $this->smarty->getTemplateVars('themeconf');
265    return isset($tc[$val]) ? $tc[$val] : '';
266  }
267
268  /**
269   * Sets the template filename for handle.
270   *
271   * @param string $handle
272   * @param string $filename
273   * @return bool
274   */
275  function set_filename($handle, $filename)
276  {
277    return $this->set_filenames( array($handle=>$filename) );
278  }
279
280  /**
281   * Sets the template filenames for handles.
282   *
283   * @param string[] $filename_array hashmap of handle=>filename
284   * @return true
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))
296      {
297        unset($this->files[$handle]);
298      }
299      else
300      {
301        $this->files[$handle] = $this->get_extent($filename, $handle);
302      }
303    }
304    return true;
305  }
306
307  /**
308   * Sets template extention filename for handles.
309   *
310   * @param string $filename
311   * @param mixed $param
312   * @param string $dir
313   * @param bool $overwrite
314   * @param string $theme
315   * @return bool
316   */
317  function set_extent($filename, $param, $dir='', $overwrite=true, $theme='N/A')
318  {
319    return $this->set_extents(array($filename => $param), $dir, $overwrite);
320  }
321
322  /**
323   * Sets template extentions filenames for handles.
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
330   */
331  function set_extents($filename_array, $dir='', $overwrite=true, $theme='N/A')
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];
343        $thm = $value[2];
344      }
345      elseif (is_string($value))
346      {
347        $handle = $value;
348        $param = 'N/A';
349        $thm = 'N/A';
350      }
351      else
352      {
353        return false;
354      }
355
356      if ((stripos(implode('',array_keys($_GET)), '/'.$param) !== false or $param == 'N/A')
357        and ($thm == $theme or $thm == 'N/A')
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  }
366
367  /**
368   * Returns template extension if exists.
369   *
370   * @param string $filename should be empty!
371   * @param string $handle
372   * @return string
373   */
374  function get_extent($filename='', $handle='')
375  {
376    if (isset($this->extents[$handle]))
377    {
378      $filename = $this->extents[$handle];
379    }
380    return $filename;
381  }
382
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)
392  {
393    $this->smarty->assign( $tpl_var, $value );
394  }
395
396  /**
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
404   */
405  function assign_var_from_handle($varname, $handle)
406  {
407    $this->assign($varname, $this->parse($handle, true));
408    return true;
409  }
410
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   */
419  function append($tpl_var, $value=null, $merge=false)
420  {
421    $this->smarty->append( $tpl_var, $value, $merge );
422  }
423
424  /**
425   * Performs a string concatenation.
426   *
427   * @param string $tpl_var
428   * @param string $value
429   */
430  function concat($tpl_var, $value)
431  {
432    $this->assign($tpl_var,
433      $this->smarty->getTemplateVars($tpl_var) . $value);
434  }
435
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   */
442  function clear_assign($tpl_var)
443  {
444    $this->smarty->clearAssign( $tpl_var );
445  }
446
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)
454  {
455    return $this->smarty->getTemplateVars( $tpl_var );
456  }
457
458  /**
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
465   */
466  function parse($handle, $return=false)
467  {
468    if ( !isset($this->files[$handle]) )
469    {
470      fatal_error("Template->parse(): Couldn't load template file for handle $handle");
471    }
472
473    $this->smarty->assign( 'ROOT_URL', get_root_url() );
474
475    $save_compile_id = $this->smarty->compile_id;
476    $this->load_external_filters($handle);
477
478    global $conf, $lang_info;
479    if ( $conf['compiled_template_cache_language'] and isset($lang_info['code']) )
480    {
481      $this->smarty->compile_id .= '_'.$lang_info['code'];
482    }
483
484    $v = $this->smarty->fetch($this->files[$handle]);
485
486    $this->smarty->compile_id = $save_compile_id;
487    $this->unload_external_filters($handle);
488
489    if ($return)
490    {
491      return $v;
492    }
493    $this->output .= $v;
494  }
495
496  /**
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
501   */
502  function pparse($handle)
503  {
504    $this->parse($handle, false);
505    $this->flush();
506  }
507
508  /**
509   * Load and compile JS & CSS into the template and sends the output to the browser.
510   */
511  function flush()
512  {
513    if (!$this->scriptLoader->did_head())
514    {
515      $pos = strpos( $this->output, self::COMBINED_SCRIPTS_TAG );
516      if ($pos !== false)
517      {
518          $scripts = $this->scriptLoader->get_head_scripts();
519          $content = array();
520          foreach ($scripts as $script)
521          {
522              $content[]=
523                  '<script type="text/javascript" src="'
524                  . self::make_script_src($script)
525                  .'"></script>';
526          }
527
528          $this->output = substr_replace( $this->output, implode( "\n", $content ), $pos, strlen(self::COMBINED_SCRIPTS_TAG) );
529      } //else maybe error or warning ?
530    }
531
532    $css = $this->cssLoader->get_css();
533
534    $content = array();
535    foreach( $css as $combi )
536    {
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
541      $href = trigger_change('combined_css', $href, $combi);
542      $content[] = '<link rel="stylesheet" type="text/css" href="'.$href.'">';
543    }
544    $this->output = str_replace(self::COMBINED_CSS_TAG,
545        implode( "\n", $content ),
546        $this->output );
547    $this->cssLoader->clear();
548
549    if ( count($this->html_head_elements) || strlen($this->html_style) )
550    {
551      $search = "\n</head>";
552      $pos = strpos( $this->output, $search );
553      if ($pos !== false)
554      {
555        $rep = "\n".implode( "\n", $this->html_head_elements );
556        if (strlen($this->html_style))
557        {
558          $rep.='<style type="text/css">'.$this->html_style.'</style>';
559        }
560        $this->output = substr_replace( $this->output, $rep, $pos, 0 );
561      } //else maybe error or warning ?
562      $this->html_head_elements = array();
563      $this->html_style = '';
564    }
565
566    echo $this->output;
567    $this->output='';
568  }
569
570  /**
571   * Same as flush() but with optional debugging.
572   * @see Template::flush()
573   */
574  function p()
575  {
576    $this->flush();
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        );
586      Smarty_Internal_Debug::display_debug($this->smarty);
587    }
588  }
589
590  /**
591   * Eval a temp string to retrieve the original PHP value.
592   *
593   * @param string $str
594   * @return mixed
595   */
596  static function get_php_str_val($str)
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
610  /**
611   * "translate" variable modifier.
612   * Usage :
613   *    - {'Comment'|translate}
614   *    - {'%d comments'|translate:$count}
615   * @see l10n()
616   *
617   * @param array $params
618   * @return string
619   */
620  static function modcompiler_translate($params)
621  {
622    global $conf, $lang;
623
624    switch (count($params))
625    {
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      ) {
631        return var_export($lang[$key], true);
632      }
633      return 'l10n('.$params[0].')';
634
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)).')';
645    }
646  }
647
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   */
657  static function modcompiler_translate_dec($params)
658  {
659    global $conf, $lang, $lang_info;
660    if ($conf['compiled_template_cache_language'])
661    {
662      $ret = 'sprintf(';
663      if ($lang_info['zero_plural'])
664      {
665        $ret .= '($tmp=('.$params[0].'))>1||$tmp==0';
666      }
667      else
668      {
669        $ret .= '($tmp=('.$params[0].'))>1';
670      }
671      $ret .= '?';
672      $ret .= self::modcompiler_translate( array($params[2]) );
673      $ret .= ':';
674      $ret .= self::modcompiler_translate( array($params[1]) );
675      $ret .= ',$tmp';
676      $ret .= ')';
677      return $ret;
678    }
679    return 'l10n_dec('.$params[1].','.$params[2].','.$params[0].')';
680  }
681
682  /**
683   * "explode" variable modifier.
684   * Usage :
685   *    - {assign var=valueExploded value=$value|explode:','}
686   *
687   * @param string $text
688   * @param string $delimiter
689   * @return array
690   */
691  static function mod_explode($text, $delimiter=',')
692  {
693    return explode($delimiter, $text);
694  }
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  }
710
711  /**
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
717   */
718  function block_html_head($params, $content)
719  {
720    $content = trim($content);
721    if ( !empty($content) )
722    { // second call
723      $this->html_head_elements[] = $content;
724    }
725  }
726
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   */
734  function block_html_style($params, $content)
735  {
736    $content = trim($content);
737    if ( !empty($content) )
738    { // second call
739      $this->html_style .= "\n".$content;
740    }
741  }
742
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   */
757  function func_define_derivative($params, $smarty)
758  {
759    !empty($params['name']) or fatal_error('define_derivative missing name');
760    if (isset($params['type']))
761    {
762      $derivative = ImageStdParams::get_by_type($params['type']);
763      $smarty->assign( $params['name'], $derivative);
764      return;
765    }
766    !empty($params['width']) or fatal_error('define_derivative missing width');
767    !empty($params['height']) or fatal_error('define_derivative missing height');
768
769    $w = intval($params['width']);
770    $h = intval($params['height']);
771    $crop = 0;
772    $minw=null;
773    $minh=null;
774
775    if (isset($params['crop']))
776    {
777      if (is_bool($params['crop']))
778      {
779        $crop = $params['crop'] ? 1:0;
780      }
781      else
782      {
783        $crop = round($params['crop']/100, 2);
784      }
785
786      if ($crop)
787      {
788        $minw = empty($params['min_width']) ? $w : intval($params['min_width']);
789        $minw <= $w or fatal_error('define_derivative invalid min_width');
790        $minh = empty($params['min_height']) ? $h : intval($params['min_height']);
791        $minh <= $h or fatal_error('define_derivative invalid min_height');
792      }
793    }
794
795    $smarty->assign( $params['name'], ImageStdParams::get_custom($w, $h, $crop, $minw, $minh) );
796  }
797
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   */
810  function func_combine_script($params)
811  {
812    if (!isset($params['id']))
813    {
814      trigger_error("combine_script: missing 'id' parameter", E_USER_ERROR);
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;
824        default: trigger_error("combine_script: invalid 'load' parameter", E_USER_ERROR);
825      }
826    }
827
828    $this->scriptLoader->add( $params['id'], $load,
829      empty($params['require']) ? array() : explode( ',', $params['require'] ),
830      @$params['path'],
831      isset($params['version']) ? $params['version'] : 0,
832      @$params['template']);
833  }
834
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   */
842  function func_get_combined_scripts($params)
843  {
844    if (!isset($params['load']))
845    {
846      trigger_error("get_combined_scripts: missing 'load' parameter", E_USER_ERROR);
847    }
848    $load = $params['load']=='header' ? 0 : 1;
849    $content = array();
850
851    if ($load==0)
852    {
853      return self::COMBINED_SCRIPTS_TAG;
854    }
855    else
856    {
857      $scripts = $this->scriptLoader->get_footer_scripts();
858      foreach ($scripts[0] as $script)
859      {
860        $content[]=
861          '<script type="text/javascript" src="'
862          . self::make_script_src($script)
863          .'"></script>';
864      }
865      if (count($this->scriptLoader->inline_scripts))
866      {
867        $content[]= '<script type="text/javascript">//<![CDATA[
868';
869        $content = array_merge($content, $this->scriptLoader->inline_scripts);
870        $content[]= '//]]></script>';
871      }
872
873      if (count($scripts[1]))
874      {
875        $content[]= '<script type="text/javascript">';
876        $content[]= '(function() {
877var s,after = document.getElementsByTagName(\'script\')[document.getElementsByTagName(\'script\').length-1];';
878        foreach ($scripts[1] as $id => $script)
879        {
880          $content[]=
881            's=document.createElement(\'script\'); s.type=\'text/javascript\'; s.async=true; s.src=\''
882            . self::make_script_src($script)
883            .'\';';
884          $content[]= 'after = after.parentNode.insertBefore(s, after);';
885        }
886        $content[]= '})();';
887        $content[]= '</script>';
888      }
889    }
890    return implode("\n", $content);
891  }
892
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)
900  {
901    $ret = '';
902    if ( $script->is_remote() )
903      $ret = $script->path;
904    else
905    {
906      $ret = get_root_url().$script->path;
907      if ($script->version!==false)
908      {
909        $ret.= '?v'. ($script->version ? $script->version : PHPWG_VERSION);
910      }
911    }
912    // trigger the event for eventual use of a cdn
913    $ret = trigger_change('combined_script', $ret, $script);
914    return embellish_url($ret);
915  }
916
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   */
924  function block_footer_script($params, $content)
925  {
926    $content = trim($content);
927    if ( !empty($content) )
928    { // second call
929
930      $this->scriptLoader->add_inline(
931        $content,
932        empty($params['require']) ? array() : explode(',', $params['require'])
933      );
934    }
935  }
936
937  /**
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   */
948  function func_combine_css($params)
949  {
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
960    $this->cssLoader->add($params['id'], $params['path'], isset($params['version']) ? $params['version'] : 0, (int)@$params['order'], (bool)@$params['template']);
961  }
962
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   */
969  function func_get_combined_css($params)
970  {
971    return self::COMBINED_CSS_TAG;
972  }
973
974  /**
975   * Declares a Smarty prefilter from a plugin, allowing it to modify template
976   * source before compilation and without changing core files.
977   * They will be processed by weight ascending.
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
983   */
984  function set_prefilter($handle, $callback, $weight=50)
985  {
986    $this->external_filters[$handle][$weight][] = array('pre', $callback);
987    ksort($this->external_filters[$handle]);
988  }
989
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   */
999  function set_postfilter($handle, $callback, $weight=50)
1000  {
1001    $this->external_filters[$handle][$weight][] = array('post', $callback);
1002    ksort($this->external_filters[$handle]);
1003  }
1004
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   */
1014  function set_outputfilter($handle, $callback, $weight=50)
1015  {
1016    $this->external_filters[$handle][$weight][] = array('output', $callback);
1017    ksort($this->external_filters[$handle]);
1018  }
1019
1020  /**
1021   * Register the filters for the tpl file.
1022   *
1023   * @param string $handle
1024   */
1025  function load_external_filters($handle)
1026  {
1027    if (isset($this->external_filters[$handle]))
1028    {
1029      $compile_id = '';
1030      foreach ($this->external_filters[$handle] as $filters)
1031      {
1032        foreach ($filters as $filter)
1033        {
1034          list($type, $callback) = $filter;
1035          $compile_id .= $type.( is_array($callback) ? implode('', $callback) : $callback );
1036          $this->smarty->registerFilter($type, $callback);
1037        }
1038      }
1039      $this->smarty->compile_id .= '.'.base_convert(crc32($compile_id), 10, 36);
1040    }
1041  }
1042
1043  /**
1044   * Unregister the filters for the tpl file.
1045   *
1046   * @param string $handle
1047   */
1048  function unload_external_filters($handle)
1049  {
1050    if (isset($this->external_filters[$handle]))
1051    {
1052      foreach ($this->external_filters[$handle] as $filters)
1053      {
1054        foreach ($filters as $filter)
1055        {
1056          list($type, $callback) = $filter;
1057          $this->smarty->unregisterFilter($type, $callback);
1058        }
1059      }
1060    }
1061  }
1062
1063  /**
1064   * @toto : description of Template::prefilter_white_space
1065   *
1066   * @param string $source
1067   * @param Smarty $smarty
1068   * @param return string
1069   */
1070  static function prefilter_white_space($source, $smarty)
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();
1078    $tags = array('if','foreach','section','footer_script');
1079    foreach($tags as $tag)
1080    {
1081      $regex[] = "#^[ \t]+($ldq$tag"."[^$ld$rd]*$rdq)\s*$#m";
1082      $regex[] = "#^[ \t]+($ldq/$tag$rdq)\s*$#m";
1083    }
1084    $tags = array('include','else','combine_script','html_head');
1085    foreach($tags as $tag)
1086    {
1087      $regex[] = "#^[ \t]+($ldq$tag"."[^$ld$rd]*$rdq)\s*$#m";
1088    }
1089    $source = preg_replace( $regex, "$1", $source);
1090    return $source;
1091  }
1092
1093  /**
1094   * Postfilter used when $conf['compiled_template_cache_language'] is true.
1095   *
1096   * @param string $source
1097   * @param Smarty $smarty
1098   * @param return string
1099   */
1100  static function postfilter_language($source, $smarty)
1101  {
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);
1107    return $source;
1108  }
1109
1110  /**
1111   * Prefilter used to add theme local CSS files.
1112   *
1113   * @param string $source
1114   * @param Smarty $smarty
1115   * @param return string
1116   */
1117  static function prefilter_local_css($source, $smarty)
1118  {
1119    $css = array();
1120    foreach ($smarty->getTemplateVars('themes') as $theme)
1121    {
1122      $f = PWG_LOCAL_DIR.'css/'.$theme['id'].'-rules.css';
1123      if (file_exists(PHPWG_ROOT_PATH.$f))
1124      {
1125        $css[] = "{combine_css path='$f' order=10}";
1126      }
1127    }
1128    $f = PWG_LOCAL_DIR.'css/rules.css';
1129    if (file_exists(PHPWG_ROOT_PATH.$f))
1130    {
1131      $css[] = "{combine_css path='$f' order=10}";
1132    }
1133
1134    if (!empty($css))
1135    {
1136      $source = str_replace("{get_combined_css}", implode( "\n", $css )."\n{get_combined_css}", $source);
1137    }
1138
1139    return $source;
1140  }
1141
1142  /**
1143   * Loads the configuration file from a theme directory and returns it.
1144   *
1145   * @param string $dir
1146   * @return array
1147   */
1148  function load_themeconf($dir)
1149  {
1150    global $themeconfs, $conf;
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  }
1162
1163  /**
1164   * Registers a button to be displayed on picture page.
1165   *
1166   * @param string $content
1167   * @param int $rank
1168   */
1169  function add_picture_button($content, $rank=BUTTONS_RANK_NEUTRAL)
1170  {
1171    $this->picture_buttons[$rank][] = $content;
1172  }
1173
1174  /**
1175   * Registers a button to be displayed on index pages.
1176   *
1177   * @param string $content
1178   * @param int $rank
1179   */
1180  function add_index_button($content, $rank=BUTTONS_RANK_NEUTRAL)
1181  {
1182    $this->index_buttons[$rank][] = $content;
1183  }
1184
1185  /**
1186   * Assigns PLUGIN_PICTURE_BUTTONS template variable with registered picture buttons.
1187   */
1188  function parse_picture_buttons()
1189  {
1190    if (!empty($this->picture_buttons))
1191    {
1192      ksort($this->picture_buttons);
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);
1199
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          // ));
1207    }
1208  }
1209
1210  /**
1211   * Assigns PLUGIN_INDEX_BUTTONS template variable with registered index buttons.
1212   */
1213  function parse_index_buttons()
1214  {
1215    if (!empty($this->index_buttons))
1216    {
1217      ksort($this->index_buttons);
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);
1224
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          // ));
1232    }
1233  }
1234}
1235
1236
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{
1243  /**
1244   * @deprecated use "translate" modifier
1245   */
1246  function l10n($text)
1247  {
1248    return l10n($text);
1249  }
1250
1251  /**
1252   * @deprecated use "translate_dec" modifier
1253   */
1254  function l10n_dec($s, $p, $v)
1255  {
1256    return l10n_dec($s, $p, $v);
1257  }
1258
1259  /**
1260   * @deprecated use "translate" or "sprintf" modifier
1261   */
1262  function sprintf()
1263  {
1264    $args = func_get_args();
1265    return call_user_func_array('sprintf',  $args );
1266  }
1267
1268  /**
1269   * @param string $type
1270   * @param array $img
1271   * @return DerivativeImage
1272   */
1273  function derivative($type, $img)
1274  {
1275    return new DerivativeImage($type, $img);
1276  }
1277
1278  /**
1279   * @param string $type
1280   * @param array $img
1281   * @return string
1282   */
1283  function derivative_url($type, $img)
1284  {
1285    return DerivativeImage::url($type, $img);
1286  }
1287}
1288
1289
1290/**
1291 * A Combinable represents a JS or CSS file ready for cobination and minification.
1292 */
1293class Combinable
1294{
1295  /** @var string */
1296  public $id;
1297  /** @var string */
1298  public $path;
1299  /** @var string */
1300  public $version;
1301  /** @var bool */
1302  public $is_template;
1303
1304  /**
1305   * @param string $id
1306   * @param string $path
1307   * @param string $version
1308   */
1309  function __construct($id, $path, $version=0)
1310  {
1311    $this->id = $id;
1312    $this->set_path($path);
1313    $this->version = $version;
1314    $this->is_template = false;
1315  }
1316
1317  /**
1318   * @param string $path
1319   */
1320  function set_path($path)
1321  {
1322    if (!empty($path))
1323      $this->path = $path;
1324  }
1325
1326  /**
1327   * @return bool
1328   */
1329  function is_remote()
1330  {
1331    return url_is_remote($this->path) || strncmp($this->path, '//', 2)==0;
1332  }
1333}
1334
1335/**
1336 * Implementation of Combinable for JS files.
1337 */
1338final class Script extends Combinable
1339{
1340  /** @var int 0,1,2 */
1341  public $load_mode;
1342  /** @var array */
1343  public $precedents;
1344  /** @var array */
1345  public $extra;
1346
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())
1355  {
1356    parent::__construct($id, $path, $version);
1357    $this->load_mode = $load_mode;
1358    $this->precedents = $precedents;
1359    $this->extra = array();
1360  }
1361}
1362
1363/**
1364 * Implementation of Combinable for CSS files.
1365 */
1366final class Css extends Combinable
1367{
1368  /** @var int */
1369  public $order;
1370
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)
1378  {
1379    parent::__construct($id, $path, $version);
1380    $this->order = $order;
1381  }
1382}
1383
1384
1385/**
1386 * Manages a list of CSS files and combining them in a unique file.
1387 */
1388class CssLoader
1389{
1390  /** @param Css[] */
1391  private $registered_css;
1392  /** @param int used to keep declaration order */
1393  private $counter;
1394
1395  function __construct()
1396  {
1397    $this->clear();
1398  }
1399
1400  function clear()
1401  {
1402    $this->registered_css = array();
1403    $this->counter = 0;
1404  }
1405
1406  /**
1407   * @return Combinable[] array of combined CSS.
1408   */
1409  function get_css()
1410  {
1411    uasort($this->registered_css, array('CssLoader', 'cmp_by_order'));
1412    $combiner = new FileCombiner('css', $this->registered_css);
1413    return $combiner->combine();
1414  }
1415
1416  /**
1417   * Callback for CSS files sorting.
1418   */
1419  private static function cmp_by_order($a, $b)
1420  {
1421    return $a->order - $b->order;
1422  }
1423
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   */
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];
1447      if ($css->order<$order*1000 || version_compare($css->version, $version)<0)
1448      {
1449        unset($this->registered_css[$id]);
1450        $this->add($id, $path, $version, $order, $is_template);
1451      }
1452    }
1453  }
1454}
1455
1456
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 */
1461class ScriptLoader
1462{
1463  /** @var Script[] */
1464  private $registered_scripts;
1465  /** @var string[] */
1466  public $inline_scripts;
1467
1468  /** @var bool */
1469  private $did_head;
1470  /** @var bool */
1471  private $head_done_scripts;
1472  /** @var bool */
1473  private $did_footer;
1474
1475  private static $known_paths = array(
1476      'core.scripts' => 'themes/default/js/scripts.js',
1477      'jquery' => 'themes/default/js/jquery.min.js',
1478      'jquery.ui' => 'themes/default/js/ui/minified/jquery.ui.core.min.js',
1479      'jquery.ui.effect' => 'themes/default/js/ui/minified/jquery.ui.effect.min.js',
1480    );
1481
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
1488  function __construct()
1489  {
1490    $this->clear();
1491  }
1492
1493  function clear()
1494  {
1495    $this->registered_scripts = array();
1496    $this->inline_scripts = array();
1497    $this->head_done_scripts = array();
1498    $this->did_head = $this->did_footer = false;
1499  }
1500
1501  /**
1502   * @return bool
1503   */
1504  function did_head()
1505  {
1506    return $this->did_head;
1507  }
1508
1509  /**
1510   * @return Script[]
1511   */
1512  function get_all()
1513  {
1514    return $this->registered_scripts;
1515  }
1516
1517  /**
1518   * @param string $code
1519   * @param string[] $require
1520   */
1521  function add_inline($code, $require)
1522  {
1523    !$this->did_footer || trigger_error("Attempt to add inline script but the footer has been written", E_USER_WARNING);
1524    if(!empty($require))
1525    {
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      }
1534    }
1535    $this->inline_scripts[] = $code;
1536  }
1537
1538  /**
1539   * @param string $id
1540   * @param int $load_mode
1541   * @param string[] $require
1542   * @param string $path
1543   * @param string $version
1544   */
1545  function add($id, $load_mode, $require, $path, $version=0, $is_template=false)
1546  {
1547    if ($this->did_head && $load_mode==0)
1548    {
1549      trigger_error("Attempt to add script $id but the head has been written", E_USER_WARNING);
1550    }
1551    elseif ($this->did_footer)
1552    {
1553      trigger_error("Attempt to add script $id but the footer has been written", E_USER_WARNING);
1554    }
1555    if (! isset( $this->registered_scripts[$id] ) )
1556    {
1557      $script = new Script($load_mode, $id, $path, $version, $require);
1558      $script->is_template = $is_template;
1559      self::fill_well_known($id, $script);
1560      $this->registered_scripts[$id] = $script;
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      }
1575    }
1576    else
1577    {
1578      $script = $this->registered_scripts[$id];
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
1591  /**
1592   * Returns combined scripts loaded in header.
1593   *
1594   * @return Combinable[]
1595   */
1596  function get_head_scripts()
1597  {
1598    self::check_load_dep($this->registered_scripts);
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) )
1611        $this->head_done_scripts[$id] = $script;
1612      else
1613        trigger_error("Script $id has an undefined path", E_USER_WARNING);
1614    }
1615    $this->did_head = true;
1616    return self::do_combine($this->head_done_scripts, 0);
1617  }
1618
1619  /**
1620   * Returns combined scripts loaded in footer.
1621   *
1622   * @return Combinable[]
1623   */
1624  function get_footer_scripts()
1625  {
1626    if (!$this->did_head)
1627    {
1628      self::check_load_dep($this->registered_scripts);
1629    }
1630    $this->did_footer = true;
1631    $todo = array();
1632    foreach( $this->registered_scripts as $id => $script)
1633    {
1634      if (!isset($this->head_done_scripts[$id]))
1635      {
1636        $todo[$id] = $script;
1637      }
1638    }
1639
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    }
1652    return array( self::do_combine($result[0],1), self::do_combine($result[1],2) );
1653  }
1654
1655  /**
1656   * @param Script[] $scripts
1657   * @param int $load_mode
1658   * @return Combinable[]
1659   */
1660  private static function do_combine($scripts, $load_mode)
1661  {
1662    $combiner = new FileCombiner('js', $scripts);
1663    return $combiner->combine();
1664  }
1665
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   */
1672  private static function check_load_dep($scripts)
1673  {
1674    global $conf;
1675    do
1676    {
1677      $changed = false;
1678      foreach( $scripts as $id => $script)
1679      {
1680        $load = $script->load_mode;
1681        foreach( $script->precedents as $precedent)
1682        {
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          }
1690          if ($load==2 && $scripts[$precedent]->load_mode==2 && ($scripts[$precedent]->is_remote() or !$conf['template_combine_files']) )
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          }
1695        }
1696      }
1697    }
1698    while ($changed);
1699  }
1700
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   */
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    {
1715      $required_ids = array('jquery');
1716
1717      if ( strncmp($id, 'jquery.ui.effect-', 17)==0 )
1718      {
1719        $required_ids = array('jquery', 'jquery.ui.effect');
1720
1721        if ( empty($script->path) )
1722          $script->path = dirname(self::$known_paths['jquery.ui.effect'])."/$id.min.js";
1723      }
1724      elseif ( strncmp($id, 'jquery.ui.', 10)==0 )
1725      {
1726        if ( !isset(self::$ui_core_dependencies[$id]) )
1727          $required_ids = array_merge(array('jquery', 'jquery.ui'), array_keys(self::$ui_core_dependencies));
1728
1729        if ( empty($script->path) )
1730          $script->path = dirname(self::$known_paths['jquery.ui'])."/$id.min.js";
1731      }
1732
1733      foreach ($required_ids as $required_id)
1734      {
1735        if ( !in_array($required_id, $script->precedents ) )
1736          $script->precedents[] = $required_id;
1737      }
1738    }
1739  }
1740
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   */
1748  private function load_known_required_script($id, $load_mode)
1749  {
1750    if ( isset(self::$known_paths[$id]) or strncmp($id, 'jquery.ui.', 10)==0  )
1751    {
1752      $this->add($id, $load_mode, array(), null);
1753      return true;
1754    }
1755    return false;
1756  }
1757
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   */
1766  private function compute_script_topological_order($script_id, $recursion_limiter=0)
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    }
1773    $recursion_limiter<5 or fatal_error("combined script circular dependency");
1774    $script = $this->registered_scripts[$script_id];
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)
1781      $max = max($max, $this->compute_script_topological_order($precedent, $recursion_limiter+1) );
1782    $max++;
1783    return ($script->extra['order'] = $max);
1784  }
1785
1786  /**
1787   * Callback for scripts sorter.
1788   */
1789  private static function cmp_by_mode_and_order($s1, $s2)
1790  {
1791    $ret = $s1->load_mode - $s2->load_mode;
1792    if ($ret) return $ret;
1793
1794    $ret = $s1->extra['order'] - $s2->extra['order'];
1795    if ($ret) return $ret;
1796
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);
1802  }
1803}
1804
1805
1806/**
1807 * Allows merging of javascript and css files into a single one.
1808 */
1809final class FileCombiner
1810{
1811  /** @var string 'js' or 'css' */
1812  private $type;
1813  /** @var bool */
1814  private $is_css;
1815  /** @var Combinable[] */
1816  private $combinables;
1817
1818  /**
1819   * @param string $type 'js' or 'css'
1820   * @param Combinable[] $combinables
1821   */
1822  function __construct($type, $combinables=array())
1823  {
1824    $this->type = $type;
1825    $this->is_css = $type=='css';
1826    $this->combinables = $combinables;
1827  }
1828
1829  /**
1830   * Deletes all combined files from cache directory.
1831   */
1832  static function clear_combined_files()
1833  {
1834    $dir = opendir(PHPWG_ROOT_PATH.PWG_COMBINED_DIR);
1835    while ($file = readdir($dir))
1836    {
1837      if ( get_extension($file)=='js' || get_extension($file)=='css')
1838        unlink(PHPWG_ROOT_PATH.PWG_COMBINED_DIR.$file);
1839    }
1840    closedir($dir);
1841  }
1842
1843  /**
1844   * @param Combinable|Combinable[] $combinable
1845   */
1846  function add($combinable)
1847  {
1848    if (is_array($combinable))
1849    {
1850      $this->combinables = array_merge($this->combinables, $combinable);
1851    }
1852    else
1853    {
1854      $this->combinables[] = $combinable;
1855    }
1856  }
1857
1858  /**
1859   * @return Combinable[]
1860   */
1861  function combine()
1862  {
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();
1873    $ini_key = $this->is_css ? array(get_absolute_root_url(false)): array(); //because for css we modify bg url;
1874    $key = $ini_key;
1875
1876    foreach ($this->combinables as $combinable)
1877    {
1878      if ($combinable->is_remote())
1879      {
1880        $this->flush_pending($result, $pending, $key, $force);
1881        $key = $ini_key;
1882        $result[] = $combinable;
1883        continue;
1884      }
1885      elseif (!$conf['template_combine_files'])
1886      {
1887        $this->flush_pending($result, $pending, $key, $force);
1888        $key = $ini_key;
1889      }
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;
1896    }
1897    $this->flush_pending($result, $pending, $key, $force);
1898    return $result;
1899  }
1900
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   */
1909  private function flush_pending(&$result, &$pending, $key, $force)
1910  {
1911    if (count($pending)>1)
1912    {
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 = '';
1918        foreach ($pending as $combinable)
1919        {
1920          $output .= "/*BEGIN $combinable->path */\n";
1921          $output .= $this->process_combinable($combinable, true, $force);
1922          $output .= "\n";
1923        }
1924        mkgetdir( dirname(PHPWG_ROOT_PATH.$file) );
1925        file_put_contents( PHPWG_ROOT_PATH.$file, $output );
1926        @chmod(PHPWG_ROOT_PATH.$file, 0644);
1927      }
1928      $result[] = new Combinable("combi", $file, false);
1929    }
1930    elseif ( count($pending)==1)
1931    {
1932      $this->process_combinable($pending[0], false, $force);
1933      $result[] = $pending[0];
1934    }
1935    $key = array();
1936    $pending = array();
1937  }
1938
1939  /**
1940   * Process one combinable file.
1941   *
1942   * @param Combinable $combinable
1943   * @param bool $return_content
1944   * @param bool $force
1945   * @return null|string
1946   */
1947  private function process_combinable($combinable, $return_content, $force)
1948  {
1949    global $conf;
1950    if ($combinable->is_template)
1951    {
1952      if (!$return_content)
1953      {
1954        $key = array($combinable->path, $combinable->version);
1955        if ($conf['template_compile_check'])
1956          $key[] = filemtime( PHPWG_ROOT_PATH . $combinable->path );
1957        $file = PWG_COMBINED_DIR . 't' . base_convert(crc32(implode(',',$key)),10,36) . '.' . $this->type;
1958        if (!$force && file_exists(PHPWG_ROOT_PATH.$file) )
1959        {
1960          $combinable->path = $file;
1961          $combinable->version = false;
1962          return;
1963        }
1964      }
1965
1966      global $template;
1967      $handle = $this->type. '.' .$combinable->id;
1968      $template->set_filename($handle, realpath(PHPWG_ROOT_PATH.$combinable->path));
1969      trigger_notify( 'combinable_preparse', $template, $combinable, $this); //allow themes and plugins to set their own vars to template ...
1970      $content = $template->parse($handle, true);
1971
1972      if ($this->is_css)
1973        $content = self::process_css($content, $combinable->path );
1974      else
1975        $content = self::process_js($content, $combinable->path );
1976
1977      if ($return_content)
1978        return $content;
1979      file_put_contents( PHPWG_ROOT_PATH.$file, $content );
1980      $combinable->path = $file;
1981    }
1982    elseif ($return_content)
1983    {
1984      $content = file_get_contents(PHPWG_ROOT_PATH . $combinable->path);
1985      if ($this->is_css)
1986        $content = self::process_css($content, $combinable->path );
1987      else
1988        $content = self::process_js($content, $combinable->path );
1989      return $content;
1990    }
1991  }
1992
1993  /**
1994   * Process a JS file.
1995   *
1996   * @param string $js file content
1997   * @param string $file
1998   * @return string
1999   */
2000  private static function process_js($js, $file)
2001  {
2002    if (strpos($file, '.min')===false and strpos($file, '.packed')===false )
2003    {
2004      require_once(PHPWG_ROOT_PATH.'include/jshrink.class.php');
2005      try { $js = JShrink_Minifier::minify($js); } catch(Exception $e) {}
2006    }
2007    return trim($js, " \t\r\n;").";\n";
2008  }
2009
2010  /**
2011   * Process a CSS file.
2012   *
2013   * @param string $css file content
2014   * @param string $file
2015   * @return string
2016   */
2017  private static function process_css($css, $file)
2018  {
2019    $css = self::process_css_rec($css, dirname($file));
2020    if (strpos($file, '.min')===false and version_compare(PHP_VERSION, '5.2.4', '>='))
2021    {
2022      require_once(PHPWG_ROOT_PATH.'include/cssmin.class.php');
2023      $css = CssMin::minify($css, array('Variables'=>false));
2024    }
2025    $css = trigger_change('combined_css_postfilter', $css);
2026    return $css;
2027  }
2028
2029  /**
2030   * Resolves relative links in CSS file.
2031   *
2032   * @param string $css file content
2033   * @param string $dir
2034   * @return string
2035   */
2036  private static function process_css_rec($css, $dir)
2037  {
2038    static $PATTERN_URL = "#url\(\s*['|\"]{0,1}(.*?)['|\"]{0,1}\s*\)#";
2039    static $PATTERN_IMPORT = "#@import\s*['|\"]{0,1}(.*?)['|\"]{0,1};#";
2040
2041    if (preg_match_all($PATTERN_URL, $css, $matches, PREG_SET_ORDER))
2042    {
2043      $search = $replace = array();
2044      foreach ($matches as $match)
2045      {
2046        if ( !url_is_remote($match[1]) && $match[1][0] != '/' && strpos($match[1], 'data:image/')===false)
2047        {
2048          $relative = $dir . "/$match[1]";
2049          $search[] = $match[0];
2050          $replace[] = 'url('.embellish_url(get_absolute_root_url(false).$relative).')';
2051        }
2052      }
2053      $css = str_replace($search, $replace, $css);
2054    }
2055
2056    if (preg_match_all($PATTERN_IMPORT, $css, $matches, PREG_SET_ORDER))
2057    {
2058      $search = $replace = array();
2059      foreach ($matches as $match)
2060      {
2061        $search[] = $match[0];
2062        $sub_css = file_get_contents(PHPWG_ROOT_PATH . $dir . "/$match[1]");
2063        $replace[] = self::process_css_rec($sub_css, dirname($dir . "/$match[1]") );
2064      }
2065      $css = str_replace($search, $replace, $css);
2066    }
2067    return $css;
2068  }
2069}
2070
2071?>
Note: See TracBrowser for help on using the repository browser.