]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/SugarTheme/SugarTheme.php
Release 6.5.8
[Github/sugarcrm.git] / include / SugarTheme / SugarTheme.php
1 <?php
2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4  * SugarCRM Community Edition is a customer relationship management program developed by
5  * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
6  * 
7  * This program is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU Affero General Public License version 3 as published by the
9  * Free Software Foundation with the addition of the following permission added
10  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11  * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13  * 
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
17  * details.
18  * 
19  * You should have received a copy of the GNU Affero General Public License along with
20  * this program; if not, see http://www.gnu.org/licenses or write to the Free
21  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22  * 02110-1301 USA.
23  * 
24  * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25  * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
26  * 
27  * The interactive user interfaces in modified source and object code versions
28  * of this program must display Appropriate Legal Notices, as required under
29  * Section 5 of the GNU Affero General Public License version 3.
30  * 
31  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32  * these Appropriate Legal Notices must retain the display of the "Powered by
33  * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34  * technical reasons, the Appropriate Legal Notices must display the words
35  * "Powered by SugarCRM".
36  ********************************************************************************/
37
38
39 /*********************************************************************************
40
41  * Description:  Contains a variety of utility functions used to display UI
42  * components such as form headers and footers.  Intended to be modified on a per
43  * theme basis.
44  ********************************************************************************/
45
46 if(!defined('JSMIN_AS_LIB'))
47     define('JSMIN_AS_LIB', true);
48
49 require_once("include/SugarTheme/cssmin.php");
50 require_once("jssource/jsmin.php");
51 require_once('include/utils/sugar_file_utils.php');
52
53 /**
54  * Class that provides tools for working with a theme.
55  * @api
56  */
57 class SugarTheme
58 {
59     /**
60      * Theme name
61      *
62      * @var string
63      */
64     protected $name;
65
66     /**
67      * Theme description
68      *
69      * @var string
70      */
71     protected $description;
72
73     /**
74      * Defines which parent files to not include
75      *
76      * @var string
77      */
78     protected $ignoreParentFiles = array();
79
80     /**
81      * Defines which parent files to not include
82      *
83      * @var string
84      */
85     public $directionality = 'ltr';
86     /**
87      * Theme directory name
88      *
89      * @var string
90      */
91     protected $dirName;
92
93     /**
94      * Parent theme name
95      *
96      * @var string
97      */
98     protected $parentTheme;
99
100     /**
101      * Colors sets provided by the theme
102      *
103      * @deprecated only here for BC during upgrades
104      * @var array
105      */
106     protected $colors = array();
107
108     /**
109      * Font sets provided by the theme
110      *
111      * @deprecated only here for BC during upgrades
112      * @var array
113      */
114     protected $fonts  = array();
115
116     /**
117      * Maximum sugar version this theme is for; defaults to 5.5.1 as all the themes without this
118      * parameter as assumed to work thru 5.5.1
119      *
120      * @var int
121      */
122     protected $version = '5.5.1';
123
124     /**
125      * Colors used in bar charts
126      *
127      * @var array
128      */
129     protected $barChartColors = array(
130         "docBorder"             => "0xffffff",
131         "docBg1"                => "0xffffff",
132         "docBg2"                => "0xffffff",
133         "xText"                 => "0x33485c",
134         "yText"                 => "0x33485c",
135         "title"                 => "0x333333",
136         "misc"                  => "0x999999",
137         "altBorder"             => "0xffffff",
138         "altBg"                 => "0xffffff",
139         "altText"               => "0x666666",
140         "graphBorder"           => "0xcccccc",
141         "graphBg1"              => "0xf6f6f6",
142         "graphBg2"              => "0xf6f6f6",
143         "graphLines"            => "0xcccccc",
144         "graphText"             => "0x333333",
145         "graphTextShadow"       => "0xf9f9f9",
146         "barBorder"             => "0xeeeeee",
147         "barBorderHilite"       => "0x333333",
148         "legendBorder"          => "0xffffff",
149         "legendBg1"             => "0xffffff",
150         "legendBg2"             => "0xffffff",
151         "legendText"            => "0x444444",
152         "legendColorKeyBorder"  => "0x777777",
153         "scrollBar"             => "0xcccccc",
154         "scrollBarBorder"       => "0xeeeeee",
155         "scrollBarTrack"        => "0xeeeeee",
156         "scrollBarTrackBorder"  => "0xcccccc",
157         );
158
159     /**
160      * Colors used in pie charts
161      *
162      * @var array
163      */
164     protected $pieChartColors = array(
165         "docBorder"             => "0xffffff",
166         "docBg1"                => "0xffffff",
167         "docBg2"                => "0xffffff",
168         "title"                 => "0x333333",
169         "subtitle"              => "0x666666",
170         "misc"                  => "0x999999",
171         "altBorder"             => "0xffffff",
172         "altBg"                 => "0xffffff",
173         "altText"               => "0x666666",
174         "graphText"             => "0x33485c",
175         "graphTextShadow"       => "0xf9f9f9",
176         "pieBorder"             => "0xffffff",
177         "pieBorderHilite"       => "0x333333",
178         "legendBorder"          => "0xffffff",
179         "legendBg1"             => "0xffffff",
180         "legendBg2"             => "0xffffff",
181         "legendText"            => "0x444444",
182         "legendColorKeyBorder"  => "0x777777",
183         "scrollBar"             => "0xdfdfdf",
184         "scrollBarBorder"       => "0xfafafa",
185         "scrollBarTrack"        => "0xeeeeee",
186         "scrollBarTrackBorder"  => "0xcccccc",
187         );
188
189     /**
190      * Does this theme support group tabs
191      *
192      * @var bool
193      */
194     public $group_tabs;
195
196
197     /**
198      * Cache built of all css files locations
199      *
200      * @var array
201      */
202     private $_cssCache = array();
203
204     /**
205      * Cache built of all image files locations
206      *
207      * @var array
208      */
209     private $_imageCache = array();
210
211     /**
212      * Cache built of all javascript files locations
213      *
214      * @var array
215      */
216     private $_jsCache = array();
217
218     /**
219      * Cache built of all template files locations
220      *
221      * @var array
222      */
223     private $_templateCache = array();
224
225         /**
226          * Cache built of sprite meta data
227          *
228          * @var array
229          */
230         private $_spriteCache = array();
231
232     /**
233      * Size of the caches after the are initialized in the constructor
234      *
235      * @var array
236      */
237     private $_initialCacheSize = array(
238         'cssCache'      => 0,
239         'imageCache'    => 0,
240         'jsCache'       => 0,
241         'templateCache' => 0,
242                 'spriteCache'   => 0,
243         );
244
245     /**
246      * Controls whether or not to clear the cache on destroy; defaults to false
247      */
248     private $_clearCacheOnDestroy = false;
249
250     private $imageExtensions = array(
251             'gif',
252             'png',
253             'jpg',
254             'tif',
255             'bmp',
256     );
257
258     /**
259      * Constructor
260      *
261      * Sets the theme properties from the defaults passed to it, and loads the file path cache from an external cache
262      *
263      * @param  $defaults string defaults for the current theme
264      */
265     public function __construct(
266         $defaults
267         )
268     {
269         // apply parent theme's properties first
270         if ( isset($defaults['parentTheme']) ) {
271             $themedef = array();
272             include("themes/{$defaults['parentTheme']}/themedef.php");
273             foreach ( $themedef as $key => $value ) {
274                 if ( property_exists(__CLASS__,$key) ) {
275                     // For all arrays ( except colors and fonts ) you can just specify the items
276                     // to change instead of all of the values
277                     if ( is_array($this->$key) && !in_array($key,array('colors','fonts')) )
278                         $this->$key = array_merge($this->$key,$value);
279                     else
280                         $this->$key = $value;
281                 }
282             }
283         }
284         foreach ( $defaults as $key => $value ) {
285             if ( property_exists(__CLASS__,$key) ) {
286                 // For all arrays ( except colors and fonts ) you can just specify the items
287                 // to change instead of all of the values
288                 if ( is_array($this->$key) && !in_array($key,array('colors','fonts')) )
289                     $this->$key = array_merge($this->$key,$value);
290                 else
291                     $this->$key = $value;
292             }
293         }
294         if ( !inDeveloperMode() ) {
295             if ( sugar_is_file($cachedfile = sugar_cached($this->getFilePath().'/pathCache.php'))) {
296                 $caches = unserialize(file_get_contents($cachedfile));
297                 if ( isset($caches['jsCache']) )
298                     $this->_jsCache       = $caches['jsCache'];
299                 if ( isset($caches['cssCache']) )
300                     $this->_cssCache      = $caches['cssCache'];
301                 if ( isset($caches['imageCache']) )
302                     $this->_imageCache    = $caches['imageCache'];
303                 if ( isset($caches['templateCache']) )
304                     $this->_templateCache = $caches['templateCache'];
305             }
306             $cachedfile = sugar_cached($this->getFilePath().'/spriteCache.php');
307                         if(!empty($GLOBALS['sugar_config']['use_sprites']) && sugar_is_file($cachedfile)) {
308                                 $this->_spriteCache = unserialize(sugar_file_get_contents($cachedfile));
309                         }
310         }
311         $this->_initialCacheSize = array(
312             'jsCache'       => count($this->_jsCache),
313             'cssCache'      => count($this->_cssCache),
314             'imageCache'    => count($this->_imageCache),
315             'templateCache' => count($this->_templateCache),
316                         'spriteCache'   => count($this->_spriteCache),
317             );
318     }
319
320     /**
321          * This is needed to prevent unserialize vulnerability
322      */
323     public function __wakeup()
324     {
325         // clean all properties
326         foreach(get_object_vars($this) as $k => $v) {
327             $this->$k = null;
328         }
329         throw new Exception("Not a serializable object");
330     }
331
332     /**
333      * Destructor
334      * Here we'll write out the internal file path caches to an external cache of some sort.
335      */
336     public function __destruct()
337     {
338         // Set the current directory to one which we expect it to be (i.e. the root directory of the install
339         $dir = realpath(dirname(__FILE__) . '/../..');
340         static $includePathIsPatched = false;
341         if ($includePathIsPatched == false)
342         {
343             $path = explode(PATH_SEPARATOR, get_include_path());
344             if (in_array($dir, $path) == false)
345             {
346                 set_include_path($dir . PATH_SEPARATOR . get_include_path());
347             }
348             $includePathIsPatched = true;
349         }
350         chdir($dir); // destruct can be called late, and chdir could change
351         $cachedir = sugar_cached($this->getFilePath());
352         sugar_mkdir($cachedir, 0775, true);
353         // clear out the cache on destroy if we are asked to
354         if ( $this->_clearCacheOnDestroy ) {
355
356             if (is_file("$cachedir/pathCache.php"))
357                 unlink("$cachedir/pathCache.php");
358                         if (is_file("$cachedir/spriteCache.php"))
359                                 unlink("$cachedir/spriteCache.php");
360
361         }
362         elseif ( !inDeveloperMode() ) {
363             // only update the caches if they have been changed in this request
364             if ( count($this->_jsCache) != $this->_initialCacheSize['jsCache']
365                     || count($this->_cssCache) != $this->_initialCacheSize['cssCache']
366                     || count($this->_imageCache) != $this->_initialCacheSize['imageCache']
367                     || count($this->_templateCache) != $this->_initialCacheSize['templateCache']
368                 ) {
369                 sugar_file_put_contents(
370                     "$cachedir/pathCache.php",
371                     serialize(
372                         array(
373                             'jsCache'       => $this->_jsCache,
374                             'cssCache'      => $this->_cssCache,
375                             'imageCache'    => $this->_imageCache,
376                             'templateCache' => $this->_templateCache,
377                             )
378                         )
379                     );
380
381             }
382                         if ( count($this->_spriteCache) != $this->_initialCacheSize['spriteCache']) {
383                                 sugar_file_put_contents(
384                                         "$cachedir/spriteCache.php",
385                                         serialize($this->_spriteCache)
386                                 );
387                         }
388         }
389     }
390
391     /**
392      * Specifies what is returned when the object is cast to a string, in this case it will be the
393      * theme directory name.
394      *
395      * @return string theme directory name
396      */
397     public function __toString()
398     {
399         return $this->dirName;
400     }
401
402     /**
403      * Generic public accessor method for all the properties of the theme ( which are kept protected )
404      *
405      * @return string
406      */
407     public function __get(
408         $key
409         )
410     {
411         if ( isset($this->$key) )
412             return $this->$key;
413     }
414
415     public function __isset($key){
416         return isset($this->$key);
417
418     }
419
420     public function clearJSCache()
421     {
422         $this->_jsCache = array();
423     }
424
425     /**
426      * Clears out the caches used for this themes
427      */
428     public function clearCache()
429     {
430         $this->_clearCacheOnDestroy = true;
431     }
432
433     /**
434      * Return array of all valid fields that can be specified in the themedef.php file
435      *
436      * @return array
437      */
438     public static function getThemeDefFields()
439     {
440         return array(
441             'name',
442             'description',
443             'directionality',
444             'dirName',
445             'parentTheme',
446             'version',
447             'colors',
448             'fonts',
449             'barChartColors',
450             'pieChartColors',
451             'group_tabs',
452             'ignoreParentFiles',
453             );
454     }
455
456     /**
457      * Returns the file path of the current theme
458      *
459      * @return string
460      */
461     public function getFilePath()
462     {
463         return 'themes/'.$this->dirName;
464     }
465
466     /**
467      * Returns the image path of the current theme
468      *
469      * @return string
470      */
471     public function getImagePath()
472     {
473         return $this->getFilePath().'/images';
474     }
475
476     /**
477      * Returns the css path of the current theme
478      *
479      * @return string
480      */
481     public function getCSSPath()
482     {
483         return $this->getFilePath().'/css';
484     }
485
486     /**
487      * Returns the javascript path of the current theme
488      *
489      * @return string
490      */
491     public function getJSPath()
492     {
493         return $this->getFilePath().'/js';
494     }
495
496     /**
497      * Returns the tpl path of the current theme
498      *
499      * @return string
500      */
501     public function getTemplatePath()
502     {
503         return $this->getFilePath().'/tpls';
504     }
505
506     /**
507      * Returns the file path of the theme defaults
508      *
509      * @return string
510      */
511     public final function getDefaultFilePath()
512     {
513         return 'themes/default';
514     }
515
516     /**
517      * Returns the image path of the theme defaults
518      *
519      * @return string
520      */
521     public final function getDefaultImagePath()
522     {
523         return $this->getDefaultFilePath().'/images';
524     }
525
526     /**
527      * Returns the css path of the theme defaults
528      *
529      * @return string
530      */
531     public final function getDefaultCSSPath()
532     {
533         return $this->getDefaultFilePath().'/css';
534     }
535
536     /**
537      * Returns the template path of the theme defaults
538      *
539      * @return string
540      */
541     public final function getDefaultTemplatePath()
542     {
543         return $this->getDefaultFilePath().'/tpls';
544     }
545
546     /**
547      * Returns the javascript path of the theme defaults
548      *
549      * @return string
550      */
551     public final function getDefaultJSPath()
552     {
553         return $this->getDefaultFilePath().'/js';
554     }
555
556     /**
557      * Returns CSS for the current theme.
558      *
559      * @param  $color string optional, specifies the css color file to use if the theme supports it; defaults to cookie value or theme default
560      * @param  $font  string optional, specifies the css font file to use if the theme supports it; defaults to cookie value or theme default
561      * @return string HTML code
562      */
563     public function getCSS(
564         $color = null,
565         $font = null
566         )
567     {
568         // include style.css file
569         $html = '<link rel="stylesheet" type="text/css" href="'.$this->getCSSURL('yui.css').'" />';
570         $html .= '<link rel="stylesheet" type="text/css" href="include/javascript/jquery/themes/base/jquery.ui.all.css" />';
571         $html .= '<link rel="stylesheet" type="text/css" href="'.$this->getCSSURL('deprecated.css').'" />';
572         $html .= '<link rel="stylesheet" type="text/css" href="'.$this->getCSSURL('style.css').'" />';
573
574                 // sprites
575                 if(!empty($GLOBALS['sugar_config']['use_sprites']) && $GLOBALS['sugar_config']['use_sprites']) {
576
577                         // system wide sprites
578                         if(file_exists("cache/sprites/default/sprites.css"))
579                                 $html .= '<link rel="stylesheet" type="text/css" href="'.getJSPath('cache/sprites/default/sprites.css').'" />';
580
581                         // theme specific sprites
582                         if(file_exists("cache/sprites/{$this->dirName}/sprites.css"))
583                                 $html .= '<link rel="stylesheet" type="text/css" href="'.getJSPath('cache/sprites/'.$this->dirName.'/sprites.css').'" />';
584
585                         // parent sprites
586                         if($this->parentTheme && $parent = SugarThemeRegistry::get($this->parentTheme)) {
587                                 if(file_exists("cache/sprites/{$parent->dirName}/sprites.css"))
588                                         $html .= '<link rel="stylesheet" type="text/css" href="'.getJSPath('cache/sprites/'.$parent->dirName.'/sprites.css').'" />';
589                         }
590
591                         // repeatable sprites
592                         if(file_exists("cache/sprites/Repeatable/sprites.css"))
593                                 $html .= '<link rel="stylesheet" type="text/css" href="'.getJSPath('cache/sprites/Repeatable/sprites.css').'" />';
594                 }
595
596         // for BC during upgrade
597         if ( !empty($this->colors) ) {
598             if ( isset($_SESSION['authenticated_user_theme_color']) && in_array($_SESSION['authenticated_user_theme_color'], $this->colors))
599                 $color = $_SESSION['authenticated_user_theme_color'];
600             else
601                 $color = $this->colors[0];
602             $html .= '<link rel="stylesheet" type="text/css" href="'.$this->getCSSURL('colors.'.$color.'.css').'" id="current_color_style" />';
603         }
604
605         if ( !empty($this->fonts) ) {
606             if ( isset($_SESSION['authenticated_user_theme_font']) && in_array($_SESSION['authenticated_user_theme_font'], $this->fonts))
607                 $font = $_SESSION['authenticated_user_theme_font'];
608             else
609                 $font = $this->fonts[0];
610             $html .= '<link rel="stylesheet" type="text/css" href="'.$this->getCSSURL('fonts.'.$font.'.css').'" id="current_font_style" />';
611         }
612
613         return $html;
614     }
615
616     /**
617      * Returns javascript for the current theme
618      *
619      * @return string HTML code
620      */
621     public function getJS()
622     {
623         $styleJS = $this->getJSURL('style.js');
624         return <<<EOHTML
625 <script type="text/javascript" src="$styleJS"></script>
626 EOHTML;
627     }
628
629     /**
630      * Returns the path for the tpl file in the current theme. If not found in the current theme, will revert
631      * to looking in the base theme.
632      *
633      * @param  string $templateName tpl file name
634      * @return string path of tpl file to include
635      */
636     public function getTemplate(
637         $templateName
638         )
639     {
640         if ( isset($this->_templateCache[$templateName]) )
641             return $this->_templateCache[$templateName];
642
643         $templatePath = '';
644         if (sugar_is_file('custom/'.$this->getTemplatePath().'/'.$templateName))
645             $templatePath = 'custom/'.$this->getTemplatePath().'/'.$templateName;
646         elseif (sugar_is_file($this->getTemplatePath().'/'.$templateName))
647             $templatePath = $this->getTemplatePath().'/'.$templateName;
648         elseif (isset($this->parentTheme)
649                 && SugarThemeRegistry::get($this->parentTheme) instanceOf SugarTheme
650                 && ($filename = SugarThemeRegistry::get($this->parentTheme)->getTemplate($templateName)) != '')
651             $templatePath = $filename;
652         elseif (sugar_is_file('custom/'.$this->getDefaultTemplatePath().'/'.$templateName))
653             $templatePath = 'custom/'.$this->getDefaultTemplatePath().'/'.$templateName;
654         elseif (sugar_is_file($this->getDefaultTemplatePath().'/'.$templateName))
655             $templatePath = $this->getDefaultTemplatePath().'/'.$templateName;
656         else {
657             $GLOBALS['log']->warn("Template $templateName not found");
658             return false;
659         }
660
661         $this->_imageCache[$templateName] = $templatePath;
662
663         return $templatePath;
664     }
665
666     /**
667      * Returns an image tag for the given image.
668      *
669      * @param  string $image image name
670      * @param  string $other_attributes optional, other attributes to add to the image tag, not cached
671          * @param  string $width optional, defaults to the actual image's width
672          * @param  string $height optional, defaults to the actual image's height
673          * @param  string $ext optional, image extension (TODO can we deprecate this one ?)
674      * @param  string $alt optional, only used when image contains something useful, i.e. "Sally's profile pic"
675      * @return string HTML image tag or sprite
676      */
677     public function getImage(
678         $imageName,
679         $other_attributes = '',
680                 $width = null,
681                 $height = null,
682                 $ext = null,
683         $alt = ''
684     )
685     {
686
687         static $cached_results = array();
688
689                 // trap deprecated use of image extension
690                 if(is_null($ext)) {
691                         $imageNameExp = explode('.',$imageName);
692                         if(count($imageNameExp) == 1)
693                                 $imageName .= '.gif';
694                 } else {
695                         $imageName .= $ext;
696                 }
697
698                 // trap alt attributes in other_attributes
699                 if(preg_match('/alt=["\']([^\'"]+)["\']/i', $other_attributes))
700                         $GLOBALS['log']->debug("Sprites: alt attribute detected for $imageName");
701                 // sprite handler, makes use of own caching mechanism
702                 if(!empty($GLOBALS['sugar_config']['use_sprites']) && $GLOBALS['sugar_config']['use_sprites']) {
703                         // get sprite metadata
704                         if($sp = $this->getSpriteMeta($imageName)) {
705                                 // requested size should match
706                                 if( (!is_null($width) && $sp['width'] == $width) || (is_null($width)) &&
707                                         (!is_null($height) && $sp['height'] == $height) || (is_null($height)) )
708                                 {
709                     $other_attributes .= ' data-orig="'.$imageName.'"';
710
711                      if($sprite = $this->getSprite($sp['class'], $other_attributes, $alt))
712                      {
713                          return $sprite;
714                      }
715                                 }
716                         }
717                 }
718
719                 // img caching
720                 if(empty($cached_results[$imageName])) {
721                         $imageURL = $this->getImageURL($imageName,false);
722                         if ( empty($imageURL) )
723                                 return false;
724                 $cached_results[$imageName] = '<img src="'.getJSPath($imageURL).'" ';
725                 }
726
727                 $attr_width = (is_null($width)) ? "" : "width=\"$width\"";
728                 $attr_height = (is_null($height)) ? "" : "height=\"$height\"";
729                 return $cached_results[$imageName] . " $attr_width $attr_height $other_attributes alt=\"$alt\" />";
730     }
731
732         /**
733          * Returns sprite meta data
734          *
735          * @param  string $imageName Image filename including extension
736          * @return array  Sprite meta data
737          */
738         public function getSpriteMeta($imageName) {
739
740                 // return from cache
741             if(isset($this->_spriteCache[$imageName]))
742                         return $this->_spriteCache[$imageName];
743
744                         // sprite keys are base on imageURL
745                 $imageURL = $this->getImageURL($imageName,false);
746                 if(empty($imageURL)) {
747                         $this->_spriteCache[$imageName] = false;
748                         return false;
749                 }
750
751                 // load meta data, includes default images
752                 require_once("include/SugarTheme/SugarSprites.php");
753                 $meta = SugarSprites::getInstance();
754                 // add current theme dir
755                 $meta->loadSpriteMeta($this->dirName);
756                 // add parent theme dir
757                 if($this->parentTheme && $parent = SugarThemeRegistry::get($this->parentTheme)) {
758                         $meta->loadSpriteMeta($parent->dirName);
759                 }
760
761                 // add to cache
762                 if(isset($meta->sprites[$imageURL])) {
763                         $this->_spriteCache[$imageName] = $meta->sprites[$imageURL];
764                         // add imageURL to cache
765                         //$this->_spriteCache[$imageName]['imageURL'] = $imageURL;
766                 } else {
767                         $this->_spriteCache[$imageName] = false;
768                         $GLOBALS['log']->debug("Sprites: miss for $imageURL");
769                 }
770                 return $this->_spriteCache[$imageName];
771         }
772
773         /**
774          * Returns sprite HTML span tag
775          *
776          * @param  string class The md5 id used in the CSS sprites class
777          * @param  string attr  optional, list of additional html attributes
778          * @param  string title optional, the title (equivalent to alt on img)
779          * @return string HTML span tag
780          */
781         public function getSprite($class, $attr, $title) {
782
783                 // handle multiple class tags
784                 $class_regex = '/class=["\']([^\'"]+)["\']/i';
785                 preg_match($class_regex, $attr, $match);
786                 if(isset($match[1])) {
787                         $attr = preg_replace($class_regex, 'class="spr_'.$class.' ${1}"', $attr);
788
789                 // single class
790                 } else {
791                         $attr .= ' class="spr_'.$class.'"';
792                 }
793
794                 if($title)
795                         $attr .= ' title="'.$title.'"';
796
797                 // use </span> instead of /> to prevent weird UI results
798                 $GLOBALS['log']->debug("Sprites: generated sprite -> $attr");
799                 return "<span {$attr}></span>";
800         }
801
802         /**
803          * Returns a link HTML tag with or without an embedded image
804          */
805     public function getLink(
806                 $url,
807                 $title,
808                 $other_attributes = '',
809         $img_name = '',
810         $img_other_attributes = '',
811                 $img_width = null,
812                 $img_height = null,
813                 $img_alt = '',
814                 $img_placement = 'imageonly'
815     )
816     {
817
818                 if($img_name) {
819                         $img = $this->getImage($img_name, $img_other_attributes, $img_width, $img_height, null, $img_alt);
820                         if($img == false) {
821                                 $GLOBALS['log']->debug('Sprites: unknown image getLink');
822                                 $img = 'unknown';
823                         }
824                         switch($img_placement) {
825                                 case 'left':    $inner_html = $img."<span class='title'>".$title."</span>"; break;
826                                 case 'right':   $inner_html = "<span class='title'>".$title."</span>".$img; break;
827                                 default:                $inner_html = $img; break;
828                         }
829                 } else {
830                         $inner_html = $title;
831                 }
832
833                 return '<a href="'.$url.'" title="'.$title.'" '.$other_attributes.'>'.$inner_html.'</a>';
834
835         }
836
837     /**
838      * Returns the URL for an image in the current theme. If not found in the current theme, will revert
839      * to looking in the base theme.
840      * @param  string $imageName image file name
841      * @param  bool   $addJSPath call getJSPath() with the results to add some unique image tracking support
842      * @return string path to image
843      */
844     public function getImageURL(
845         $imageName,
846         $addJSPath = true
847         ){
848         if ( isset($this->_imageCache[$imageName]) ) {
849             if ( $addJSPath )
850                 return getJSPath($this->_imageCache[$imageName]);
851             else
852                 return $this->_imageCache[$imageName];
853         }
854         $imagePath = '';
855         if (($filename = $this->_getImageFileName('custom/'.$this->getImagePath().'/'.$imageName)) != '')
856             $imagePath = $filename;
857         elseif (($filename = $this->_getImageFileName($this->getImagePath().'/'.$imageName)) != '')
858             $imagePath = $filename;
859         elseif (isset($this->parentTheme)
860                 && SugarThemeRegistry::get($this->parentTheme) instanceOf SugarTheme
861                 && ($filename = SugarThemeRegistry::get($this->parentTheme)->getImageURL($imageName,false)) != '')
862             $imagePath = $filename;
863         elseif (($filename = $this->_getImageFileName('custom/'.$this->getDefaultImagePath().'/'.$imageName)) != '')
864             $imagePath = $filename;
865         elseif (($filename = $this->_getImageFileName($this->getDefaultImagePath().'/'.$imageName)) != '')
866             $imagePath = $filename;
867                 elseif (($filename = $this->_getImageFileName('include/images/'.$imageName)) != '')
868                         $imagePath = $filename;
869         else {
870             $GLOBALS['log']->warn("Image $imageName not found");
871             return false;
872         }
873
874         $this->_imageCache[$imageName] = $imagePath;
875
876         if ( $addJSPath )
877             return getJSPath($imagePath);
878
879         return $imagePath;
880     }
881
882     /**
883      * Checks for an image using all of the accepted image extensions
884      *
885      * @param  string $imageName image file name
886      * @return string path to image
887      */
888     protected function _getImageFileName(
889         $imageName
890         )
891     {
892         // return now if the extension matches that of which we are looking for
893         if ( sugar_is_file($imageName) )
894             return $imageName;
895         $pathParts = pathinfo($imageName);
896         foreach ( $this->imageExtensions as $extension )
897             if ( isset($pathParts['extension']) )
898                 if ( ( $extension != $pathParts['extension'] )
899                         && sugar_is_file($pathParts['dirname'].'/'.$pathParts['filename'].'.'.$extension) )
900                     return $pathParts['dirname'].'/'.$pathParts['filename'].'.'.$extension;
901
902         return '';
903     }
904
905     /**
906      * Returns the URL for the css file in the current theme. If not found in the current theme, will revert
907      * to looking in the base theme.
908      *
909      * @param  string $cssFileName css file name
910      * @param  bool   $returnURL if true, returns URL with unique image mark, otherwise returns path to the file
911      * @return string path of css file to include
912      */
913     public function getCSSURL($cssFileName, $returnURL = true)
914     {
915         if ( isset($this->_cssCache[$cssFileName]) && sugar_is_file(sugar_cached($this->_cssCache[$cssFileName])) ) {
916             if ( $returnURL )
917                 return getJSPath("cache/".$this->_cssCache[$cssFileName]);
918             else
919                 return sugar_cached($this->_cssCache[$cssFileName]);
920         }
921
922         $cssFileContents = '';
923         $defaultFileName = $this->getDefaultCSSPath().'/'.$cssFileName;
924         $fullFileName = $this->getCSSPath().'/'.$cssFileName;
925         if (isset($this->parentTheme)
926                 && SugarThemeRegistry::get($this->parentTheme) instanceOf SugarTheme
927                 && ($filename = SugarThemeRegistry::get($this->parentTheme)->getCSSURL($cssFileName,false)) != '')
928             $cssFileContents .= file_get_contents($filename);
929         else {
930             if (sugar_is_file($defaultFileName))
931                 $cssFileContents .= file_get_contents($defaultFileName);
932             if (sugar_is_file('custom/'.$defaultFileName))
933                 $cssFileContents .= file_get_contents('custom/'.$defaultFileName);
934         }
935         if (sugar_is_file($fullFileName)) {
936             $cssFileContents .= file_get_contents($fullFileName);
937         }
938         if (sugar_is_file('custom/'.$fullFileName)) {
939             $cssFileContents .= file_get_contents('custom/'.$fullFileName);
940         }
941         if (empty($cssFileContents)) {
942             $GLOBALS['log']->warn("CSS File $cssFileName not found");
943             return false;
944         }
945
946         // fix any image references that may be defined in css files
947         $cssFileContents = str_ireplace("entryPoint=getImage&",
948             "entryPoint=getImage&themeName={$this->dirName}&",
949             $cssFileContents);
950
951         // create the cached file location
952         $cssFilePath = create_cache_directory($fullFileName);
953
954         // if this is the style.css file, prepend the base.css and calendar-win2k-cold-1.css
955         // files before the theme styles
956         if ( $cssFileName == 'style.css' && !isset($this->parentTheme) ) {
957             if ( inDeveloperMode() )
958                 $cssFileContents = file_get_contents('include/javascript/yui/build/base/base.css') . $cssFileContents;
959             else
960                 $cssFileContents = file_get_contents('include/javascript/yui/build/base/base-min.css') . $cssFileContents;
961         }
962
963         // minify the css
964         if ( !inDeveloperMode() && !sugar_is_file($cssFilePath) ) {
965             $cssFileContents = cssmin::minify($cssFileContents);
966         }
967
968         // now write the css to cache
969         sugar_file_put_contents($cssFilePath,$cssFileContents);
970
971         $this->_cssCache[$cssFileName] = $fullFileName;
972
973         if ( $returnURL )
974             return getJSPath("cache/".$fullFileName);
975
976         return sugar_cached($fullFileName);
977     }
978
979     /**
980      * Returns the URL for an image in the current theme. If not found in the current theme, will revert
981      * to looking in the base theme.
982      *
983      * @param  string $jsFileName js file name
984      * @param  bool   $returnURL if true, returns URL with unique image mark, otherwise returns path to the file
985      * @return string path to js file
986      */
987     public function getJSURL($jsFileName, $returnURL = true)
988     {
989         if ( isset($this->_jsCache[$jsFileName]) && sugar_is_file(sugar_cached($this->_jsCache[$jsFileName])) ) {
990             if ( $returnURL )
991                 return getJSPath("cache/".$this->_jsCache[$jsFileName]);
992             else
993                 return sugar_cached($this->_jsCache[$jsFileName]);
994         }
995
996         $jsFileContents = '';
997         $fullFileName = $this->getJSPath().'/'.$jsFileName;
998         $defaultFileName = $this->getDefaultJSPath().'/'.$jsFileName;
999         if (isset($this->parentTheme)
1000                 && SugarThemeRegistry::get($this->parentTheme) instanceOf SugarTheme
1001                 && ($filename = SugarThemeRegistry::get($this->parentTheme)->getJSURL($jsFileName,false)) != ''    && !in_array($jsFileName,$this->ignoreParentFiles)) {
1002            $jsFileContents .= file_get_contents($filename);
1003        } else {
1004             if (sugar_is_file($defaultFileName))
1005                 $jsFileContents .= file_get_contents($defaultFileName);
1006             if (sugar_is_file('custom/'.$defaultFileName))
1007                 $jsFileContents .= file_get_contents('custom/'.$defaultFileName);
1008         }
1009         if (sugar_is_file($fullFileName))
1010             $jsFileContents .= file_get_contents($fullFileName);
1011         if (sugar_is_file('custom/'.$fullFileName))
1012             $jsFileContents .= file_get_contents('custom/'.$fullFileName);
1013         if (empty($jsFileContents)) {
1014             $GLOBALS['log']->warn("Javascript File $jsFileName not found");
1015             return false;
1016         }
1017
1018         // create the cached file location
1019         $jsFilePath = create_cache_directory($fullFileName);
1020
1021         // minify the js
1022         if ( !inDeveloperMode()&& !sugar_is_file(str_replace('.js','-min.js',$jsFilePath)) ) {
1023             $jsFileContents = SugarMin::minify($jsFileContents);
1024             $jsFilePath = str_replace('.js','-min.js',$jsFilePath);
1025             $fullFileName = str_replace('.js','-min.js',$fullFileName);
1026         }
1027
1028         // now write the js to cache
1029         sugar_file_put_contents($jsFilePath,$jsFileContents);
1030
1031         $this->_jsCache[$jsFileName] = $fullFileName;
1032
1033         if ( $returnURL )
1034             return getJSPath("cache/".$fullFileName);
1035
1036         return sugar_cached($fullFileName);
1037     }
1038
1039     /**
1040      * Returns an array of all of the images available for the current theme
1041      *
1042      * @return array
1043      */
1044     public function getAllImages()
1045     {
1046         // first, lets get all the paths of where to look
1047         $pathsToSearch = array($this->getImagePath());
1048         $theme = $this;
1049         while (isset($theme->parentTheme) && SugarThemeRegistry::get($theme->parentTheme) instanceOf SugarTheme ) {
1050             $theme = SugarThemeRegistry::get($theme->parentTheme);
1051             $pathsToSearch[] = $theme->getImagePath();
1052         }
1053         $pathsToSearch[] = $this->getDefaultImagePath();
1054
1055         // now build the array
1056         $imageArray = array();
1057         foreach ( $pathsToSearch as $path )
1058         {
1059             if (!sugar_is_dir($path)) $path = "custom/$path";
1060             if (sugar_is_dir($path) && is_readable($path) && $dir = opendir($path)) {
1061                 while (($file = readdir($dir)) !== false) {
1062                     if ($file == ".."
1063                             || $file == "."
1064                             || $file == ".svn"
1065                             || $file == "CVS"
1066                             || $file == "Attic"
1067                             )
1068                         continue;
1069                     if ( !isset($imageArray[$file]) )
1070                         $imageArray[$file] = $this->getImageURL($file,false);
1071                 }
1072                 closedir($dir);
1073             }
1074         }
1075
1076         ksort($imageArray);
1077
1078         return $imageArray;
1079     }
1080
1081 }
1082
1083 /**
1084  * Registry for all the current classes in the system
1085  */
1086 class SugarThemeRegistry
1087 {
1088     /**
1089      * Array of all themes and thier object
1090      *
1091      * @var array
1092      */
1093     private static $_themes = array();
1094
1095     /**
1096      * Name of the current theme; corresponds to an index key in SugarThemeRegistry::$_themes
1097      *
1098      * @var string
1099      */
1100     private static $_currentTheme;
1101
1102     /**
1103      * Disable the constructor since this will be a singleton
1104      */
1105     private function __construct() {}
1106
1107     /**
1108      * Adds a new theme to the registry
1109      *
1110      * @param $themedef array
1111      */
1112     public static function add(
1113         array $themedef
1114         )
1115     {
1116         // make sure the we know the sugar version
1117         if ( !isset($GLOBALS['sugar_version']) ) {
1118             include('sugar_version.php');
1119             $GLOBALS['sugar_version'] = $sugar_version;
1120         }
1121
1122         // Assume theme is designed for 5.5.x if not specified otherwise
1123         if ( !isset($themedef['version']) )
1124             $themedef['version']['regex_matches'] = array('5\.5\.*');
1125
1126         // Check to see if theme is valid for this version of Sugar; return false if not
1127         $version_ok = false;
1128         if( isset($themedef['version']['exact_matches']) ){
1129             $matches_empty = false;
1130             foreach( $themedef['version']['exact_matches'] as $match ){
1131                 if( $match == $GLOBALS['sugar_version'] ){
1132                     $version_ok = true;
1133                 }
1134             }
1135         }
1136         if( !$version_ok && isset($themedef['version']['regex_matches']) ){
1137             $matches_empty = false;
1138             foreach( $themedef['version']['regex_matches'] as $match ){
1139                 if( preg_match( "/$match/", $GLOBALS['sugar_version'] ) ){
1140                     $version_ok = true;
1141                 }
1142             }
1143         }
1144         if ( !$version_ok )
1145             return false;
1146
1147         $theme = new SugarTheme($themedef);
1148         self::$_themes[$theme->dirName] = $theme;
1149     }
1150
1151     /**
1152      * Removes a new theme from the registry
1153      *
1154      * @param $themeName string
1155      */
1156     public static function remove(
1157         $themeName
1158         )
1159     {
1160         if ( self::exists($themeName) )
1161             unset(self::$_themes[$themeName]);
1162     }
1163
1164     /**
1165      * Returns a theme object in the registry specified by the given $themeName
1166      *
1167      * @param $themeName string
1168      */
1169     public static function get(
1170         $themeName
1171         )
1172     {
1173         if ( isset(self::$_themes[$themeName]) )
1174             return self::$_themes[$themeName];
1175     }
1176
1177     /**
1178      * Returns the current theme object
1179      *
1180      * @return SugarTheme object
1181      */
1182     public static function current()
1183     {
1184         if ( !isset(self::$_currentTheme) )
1185             self::buildRegistry();
1186
1187         return self::$_themes[self::$_currentTheme];
1188     }
1189
1190     /**
1191      * Returns the default theme object
1192      *
1193      * @return SugarTheme object
1194      */
1195     public static function getDefault()
1196     {
1197         if ( !isset(self::$_currentTheme) )
1198             self::buildRegistry();
1199
1200         if ( isset($GLOBALS['sugar_config']['default_theme']) && self::exists($GLOBALS['sugar_config']['default_theme']) ) {
1201             return self::get($GLOBALS['sugar_config']['default_theme']);
1202         }
1203
1204         return self::get(array_pop(array_keys(self::availableThemes())));
1205     }
1206
1207     /**
1208      * Returns true if a theme object specified by the given $themeName exists in the registry
1209      *
1210      * @param  $themeName string
1211      * @return bool
1212      */
1213     public static function exists(
1214         $themeName
1215         )
1216     {
1217         return (self::get($themeName) !== null);
1218     }
1219
1220     /**
1221      * Sets the given $themeName to be the current theme
1222      *
1223      * @param  $themeName string
1224      */
1225     public static function set(
1226         $themeName
1227         )
1228     {
1229         if ( !self::exists($themeName) )
1230             return false;
1231
1232         self::$_currentTheme = $themeName;
1233
1234         // set some of the expected globals
1235         $GLOBALS['barChartColors'] = self::current()->barChartColors;
1236         $GLOBALS['pieChartColors'] = self::current()->pieChartColors;
1237         return true;
1238     }
1239
1240     /**
1241      * Builds the theme registry
1242      */
1243     public static function buildRegistry()
1244     {
1245         self::$_themes = array();
1246         $dirs = array("themes/","custom/themes/");
1247
1248         // check for a default themedef file
1249         $themedefDefault = array();
1250         if ( sugar_is_file("custom/themes/default/themedef.php") ) {
1251             $themedef = array();
1252             require("custom/themes/default/themedef.php");
1253             $themedefDefault = $themedef;
1254         }
1255
1256         foreach ($dirs as $dirPath ) {
1257             if (sugar_is_dir('./'.$dirPath) && is_readable('./'.$dirPath) && $dir = opendir('./'.$dirPath)) {
1258                 while (($file = readdir($dir)) !== false) {
1259                     if ($file == ".."
1260                             || $file == "."
1261                             || $file == ".svn"
1262                             || $file == "CVS"
1263                             || $file == "Attic"
1264                             || $file == "default"
1265                             || !sugar_is_dir("./$dirPath".$file)
1266                             || !sugar_is_file("./{$dirPath}{$file}/themedef.php")
1267                             )
1268                         continue;
1269                     $themedef = array();
1270                     require("./{$dirPath}{$file}/themedef.php");
1271                     $themedef = array_merge($themedef,$themedefDefault);
1272                     $themedef['dirName'] = $file;
1273                     // check for theme already existing in the registry
1274                     // if so, then it will override the current one
1275                     if ( self::exists($themedef['dirName']) ) {
1276                         $existingTheme = self::get($themedef['dirName']);
1277                         foreach ( SugarTheme::getThemeDefFields() as $field )
1278                             if ( !isset($themedef[$field]) )
1279                                 $themedef[$field] = $existingTheme->$field;
1280                         self::remove($themedef['dirName']);
1281                     }
1282                     if ( isset($themedef['name']) ) {
1283                         self::add($themedef);
1284                     }
1285                 }
1286                 closedir($dir);
1287             }
1288         }
1289         // default to setting the default theme as the current theme
1290         if ( !isset($GLOBALS['sugar_config']['default_theme']) || !self::set($GLOBALS['sugar_config']['default_theme']) ) {
1291             if ( count(self::availableThemes()) == 0 )
1292             {
1293                 sugar_die('No valid themes are found on this instance');
1294             } else {
1295                 self::set(self::getDefaultThemeKey());
1296             }
1297         }
1298     }
1299
1300
1301     /**
1302      * getDefaultThemeKey
1303      *
1304      * This function returns the default theme key.  It takes into account string casing issues that may arise
1305      * from upgrades.  It attempts to look for the Sugar theme and if not found, defaults to return the name of the last theme
1306      * in the array of available themes loaded.
1307      *
1308      * @return $defaultThemeKey String value of the default theme key to use
1309      */
1310     private static function getDefaultThemeKey()
1311     {
1312         $availableThemes = self::availableThemes();
1313         foreach($availableThemes as $key=>$theme)
1314         {
1315             if(strtolower($key) == 'sugar')
1316             {
1317                 return $key;
1318             }
1319         }
1320
1321         return array_pop(array_keys($availableThemes));
1322     }
1323
1324
1325     /**
1326      * Returns an array of available themes. Designed to be absorbed into get_select_options_with_id()
1327      *
1328      * @return array
1329      */
1330     public static function availableThemes()
1331     {
1332         $themelist = array();
1333         $disabledThemes = array();
1334         if ( isset($GLOBALS['sugar_config']['disabled_themes']) )
1335             $disabledThemes = explode(',',$GLOBALS['sugar_config']['disabled_themes']);
1336
1337         foreach ( self::$_themes as $themename => $themeobject ) {
1338             if ( in_array($themename,$disabledThemes) )
1339                 continue;
1340             $themelist[$themeobject->dirName] = $themeobject->name;
1341         }
1342         asort($themelist, SORT_STRING);
1343         return $themelist;
1344     }
1345
1346     /**
1347      * Returns an array of un-available themes. Designed used with the theme selector in the admin panel
1348      *
1349      * @return array
1350      */
1351     public static function unAvailableThemes()
1352     {
1353         $themelist = array();
1354         $disabledThemes = array();
1355         if ( isset($GLOBALS['sugar_config']['disabled_themes']) )
1356             $disabledThemes = explode(',',$GLOBALS['sugar_config']['disabled_themes']);
1357
1358         foreach ( self::$_themes as $themename => $themeobject ) {
1359             if ( in_array($themename,$disabledThemes) )
1360                 $themelist[$themeobject->dirName] = $themeobject->name;
1361         }
1362
1363         return $themelist;
1364     }
1365
1366     /**
1367      * Returns an array of all themes found in the current installation
1368      *
1369      * @return array
1370      */
1371     public static function allThemes()
1372     {
1373         $themelist = array();
1374
1375         foreach ( self::$_themes as $themename => $themeobject )
1376             $themelist[$themeobject->dirName] = $themeobject->name;
1377
1378         return $themelist;
1379     }
1380
1381     /**
1382      * Clears out the cached path locations for all themes
1383      */
1384     public static function clearAllCaches()
1385     {
1386         foreach ( self::$_themes as $themeobject ) {
1387             $themeobject->clearCache();
1388         }
1389     }
1390 }