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