Passed
Push — master ( 76374b...079192 )
by
unknown
06:47
created

Theming::getFavicon()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 18
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 6
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 18
rs 8.8333
1
<?php
2
3
require_once __DIR__ . '/class.colors.php';
4
5
// The themes are moved to a different location when released
6
// so we will define these constants for their location
7
define('THEME_PATH_' . LOAD_SOURCE, 'client/zarafa/core/themes');
8
define('THEME_PATH_' . LOAD_DEBUG, 'client/themes');
9
define('THEME_PATH_' . LOAD_RELEASE, 'client/themes');
10
11
/**
12
 * This class provides some functionality for theming grommunio Web.
13
 */
14
class Theming {
15
	/**
16
	 * A hash that is used to cache if a theme is a json theme.
17
	 *
18
	 * @property
19
	 */
20
	private static $isJsonThemeCache = [];
21
22
	/**
23
	 * A hash that is used to cache the properties of json themes.
24
	 *
25
	 * @property
26
	 */
27
	private static $jsonThemePropsCache = [];
28
29
	/**
30
	 * Retrieves all installed json themes.
31
	 *
32
	 * @return array An array with the directory names of the json themes as keys and their display names
33
	 *               as values
34
	 */
35
	public static function getJsonThemes() {
36
		$themes = [];
37
		$directoryIterator = new DirectoryIterator(BASE_PATH . PATH_PLUGIN_DIR);
38
		foreach ($directoryIterator as $info) {
39
			if ($info->isDot() || !$info->isDir()) {
40
				continue;
41
			}
42
43
			if (!Theming::isJsonTheme($info->getFileName())) {
44
				continue;
45
			}
46
47
			$themeProps = Theming::getJsonThemeProps($info->getFileName());
48
			if (empty($themeProps)) {
49
				continue;
50
			}
51
52
			$themes[$info->getFileName()] = $themeProps['display-name'] ?? $info->getFileName();
53
		}
54
55
		return $themes;
56
	}
57
58
	/**
59
	 * Returns the name of the active theme if one was found, and false otherwise.
60
	 * The active theme can be set by the admin in the config.php, or by
61
	 * the user in his settings.
62
	 *
63
	 * @return bool|string
64
	 */
65
	public static function getActiveTheme() {
66
		$theme = false;
67
		$themePath = BASE_PATH . constant('THEME_PATH_' . DEBUG_LOADER);
68
69
		// List of unified themes that don't require separate directories
70
		$unifiedThemes = ['purple', 'orange', 'lime', 'magenta', 'highcontrast', 'blue', 'teal', 'indigo', 'red', 'green', 'amber'];
71
72
		// First check if a theme was set by this user in his settings
73
		if (WebAppAuthentication::isAuthenticated()) {
74
			if (ENABLE_THEMES === false) {
0 ignored issues
show
introduced by
The condition ENABLE_THEMES === false is always false.
Loading history...
75
				$theme = THEME !== "" ? THEME : 'basic';
76
			}
77
			else {
78
				$theme = $GLOBALS['settings']->get('zarafa/v1/main/active_theme');
79
			}
80
81
			// If a theme was found, check if the theme is still installed
82
			// Remember that 'basic' is not a real theme, but the name for the default look of grommunio Web
83
			// Unified themes don't require directories, so we skip the directory check for them
84
			if (
85
				isset($theme) && !empty($theme) && $theme !== 'basic' &&
86
				!in_array($theme, $unifiedThemes) &&
87
				!is_dir($themePath . '/' . $theme) &&
88
				!is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme)
89
			) {
90
				$theme = false;
91
			}
92
		}
93
94
		// If a valid theme was not found in the settings of the user, let's see if a valid theme
95
		// was defined by the admin.
96
		if (!$theme && defined('THEME') && THEME) {
97
			// Check if it's a unified theme or if the directory exists
98
			if (in_array(THEME, $unifiedThemes)) {
99
				$theme = THEME;
100
			}
101
			else {
102
				$theme = is_dir($themePath . '/' . THEME) || is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . THEME) ? THEME : false;
103
			}
104
		}
105
106
		if (Theming::isJsonTheme($theme) && !is_array(Theming::getJsonThemeProps($theme))) {
0 ignored issues
show
introduced by
The condition is_array(Theming::getJsonThemeProps($theme)) is always true.
Loading history...
107
			// Someone made an error, we cannot read this json theme
108
			return false;
109
		}
110
111
		return $theme;
112
	}
113
114
	/**
115
	 * Returns the path to the favicon if included with the theme. If found the
116
	 * path to it will be returned. Otherwise false.
117
	 *
118
	 * @param string $theme the name of the theme for which the css will be returned.
119
	 *                      Note: This is the directory name of the theme plugin.
120
	 *
121
	 * 	 * @return bool|string
122
	 */
123
	public static function getFavicon($theme) {
124
		$themePath = constant('THEME_PATH_' . DEBUG_LOADER);
125
126
		// First check if we can find a core theme with this name
127
		if ($theme && is_dir(BASE_PATH . $themePath . '/' . $theme) && is_file(BASE_PATH . $themePath . '/' . $theme . '/favicon.ico')) {
128
			// Add a date as GET parameter, so we will fetch a new icon every day
129
			// This way themes can update the favicon and it will show the next day latest.
130
			return $themePath . '/' . $theme . '/favicon.ico?' . date('Ymd');
131
		}
132
133
		// If no core theme was found, let's try to find a theme plugin with this name
134
		if ($theme && is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme) && is_file(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme . '/favicon.ico')) {
135
			// Add a date as GET parameter, so we will fetch a new icon every day
136
			// This way themes can update the favicon and it will show the next day latest.
137
			return PATH_PLUGIN_DIR . '/' . $theme . '/favicon.ico?' . date('Ymd');
138
		}
139
140
		return false;
141
	}
142
143
	/**
144
	 * Returns the contents of the css files in the $theme as a string.
145
	 *
146
	 * @param string $theme the name of the theme for which the css will be returned.
147
	 *                      Note: This is the directory name of the theme plugin.
148
	 *
149
	 * @return string
150
	 */
151
	public static function getCss($theme) {
152
		$themePathCoreThemes = BASE_PATH . constant('THEME_PATH_' . DEBUG_LOADER);
153
		$cssFiles = [];
154
155
		// Unified themes (purple, orange, lime, magenta, highcontrast, blue, teal, indigo, red, green, amber) are now in grommunio.css
156
		// Only the dark theme still loads its own CSS file
157
		$unifiedThemes = ['purple', 'orange', 'lime', 'magenta', 'highcontrast', 'blue', 'teal', 'indigo', 'red', 'green', 'amber'];
158
		if (in_array($theme, $unifiedThemes)) {
159
			return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type string.
Loading history...
160
		}
161
162
		// First check if this is a core theme, and if it isn't, check if it is a theme plugin
163
		if ($theme && is_dir($themePathCoreThemes . '/' . $theme)) {
164
			$themePath = $themePathCoreThemes . '/' . $theme;
165
		}
166
		elseif ($theme && is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme)) {
167
			if (Theming::isJsonTheme($theme)) {
168
				return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type string.
Loading history...
169
			}
170
			$themePath = BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme;
171
		}
172
173
		if (isset($themePath)) {
174
			// Use SPL iterators to recursively traverse the css directory and find all css files
175
			$directoryIterator = new RecursiveDirectoryIterator($themePath . '/css/', FilesystemIterator::SKIP_DOTS);
176
			$iterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
177
178
			// Always rewind an iterator before using it!!! See https://bugs.php.net/bug.php?id=62914 (it might save you a couple of hours debugging)
179
			$iterator->rewind();
180
			while ($iterator->valid()) {
181
				$fileName = $iterator->getFilename();
0 ignored issues
show
Bug introduced by
The method getFilename() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

181
				/** @scrutinizer ignore-call */ 
182
    $fileName = $iterator->getFilename();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
182
				if (!$iterator->isDir() && (strtolower($iterator->getExtension()) === 'css' || str_ends_with($fileName, '.css.php'))) {
0 ignored issues
show
Bug introduced by
The method isDir() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

182
				if (!$iterator->/** @scrutinizer ignore-call */ isDir() && (strtolower($iterator->getExtension()) === 'css' || str_ends_with($fileName, '.css.php'))) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getExtension() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

182
				if (!$iterator->isDir() && (strtolower($iterator->/** @scrutinizer ignore-call */ getExtension()) === 'css' || str_ends_with($fileName, '.css.php'))) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
183
					$cssFiles[] = substr((string) $iterator->key(), strlen(BASE_PATH));
184
				}
185
				$iterator->next();
186
			}
187
		}
188
189
		// Sort the array alphabetically before adding the css
190
		sort($cssFiles);
191
192
		return $cssFiles;
193
	}
194
195
	/**
196
	 * Returns the value that is assigned to a property by the active theme
197
	 * or null otherwise.
198
	 * Currently only implemented for JSON themes.
199
	 *
200
	 * @param mixed $propName
201
	 *
202
	 * @return string the value that the active theme has set for the property,
203
	 *                or NULL
204
	 */
205
	public static function getThemeProperty($propName) {
206
		$theme = Theming::getActiveTheme();
207
		if (!Theming::isJsonTheme($theme)) {
208
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
209
		}
210
211
		$props = Theming::getJsonThemeProps($theme);
212
		if (!isset($props[$propName])) {
213
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
214
		}
215
216
		return $props[$propName];
217
	}
218
219
	/**
220
	 * Returns the color that the active theme has set for the primary color
221
	 * of the icons. Currently only supported for JSON themes.
222
	 * Note: Only SVG icons of an iconset that has defined the primary color
223
	 * can be 'recolored'.
224
	 *
225
	 * @return string the color that the active theme has set for the primary
226
	 *                color of the icons, or FALSE
227
	 */
228
	public static function getPrimaryIconColor() {
229
		$val = Theming::getThemeProperty('icons-primary-color');
230
231
		return $val ?? false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $val ?? false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
232
	}
233
234
	/**
235
	 * Returns the color that the active theme has set for the secondary color
236
	 * of the icons. Currently only supported for JSON themes.
237
	 * Note: Only SVG icons of an iconset that has defined the secondary color
238
	 * can be 'recolored'.
239
	 *
240
	 * @return string the color that the active theme has set for the secondary
241
	 *                color of the icons, or FALSE
242
	 */
243
	public static function getSecondaryIconColor() {
244
		$val = Theming::getThemeProperty('icons-secondary-color');
245
246
		return $val ?? false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $val ?? false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
247
	}
248
249
	/**
250
	 * Checks if a theme is a JSON theme. (Basically this means that it checks if a
251
	 * directory with the theme name exists and if that directory contains a file
252
	 * called theme.json).
253
	 *
254
	 * @param string $theme The name of the theme to check
255
	 *
256
	 * @return bool True if the theme is a json theme, false otherwise
257
	 */
258
	public static function isJsonTheme($theme) {
259
		if (empty($theme)) {
260
			return false;
261
		}
262
263
		if (!isset(Theming::$isJsonThemeCache[$theme])) {
264
			$themePathCoreThemes = BASE_PATH . constant('THEME_PATH_' . DEBUG_LOADER);
265
266
			// First check if this is a core theme, and if it isn't, check if it is a theme plugin
267
			if (is_dir($themePathCoreThemes . '/' . $theme)) {
268
				// We don't have core json themes, so return false
269
				Theming::$isJsonThemeCache[$theme] = false;
270
			}
271
			elseif (is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme) && is_file(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme . '/theme.json')) {
272
				Theming::$isJsonThemeCache[$theme] = true;
273
			}
274
			else {
275
				Theming::$isJsonThemeCache[$theme] = false;
276
			}
277
		}
278
279
		return Theming::$isJsonThemeCache[$theme];
280
	}
281
282
	/**
283
	 * Retrieves the properties set in the theme.json file of the theme.
284
	 *
285
	 * @param string $theme The theme for which the properties should be retrieved
286
	 *
287
	 * @return array The decoded array of properties defined in the theme.json file
288
	 */
289
	public static function getJsonThemeProps($theme) {
290
		if (!Theming::isJsonTheme($theme)) {
291
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
292
		}
293
294
		// Check if we have the props in the cache before reading the file
295
		if (!isset(Theming::$jsonThemePropsCache[$theme])) {
296
			$json = file_get_contents(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme . '/theme.json');
297
			Theming::$jsonThemePropsCache[$theme] = json_decode($json, true);
298
299
			if (json_last_error() !== JSON_ERROR_NONE) {
300
				error_log("The theme '{$theme}' does not have a valid theme.json file. " . json_last_error_msg());
301
				Theming::$jsonThemePropsCache[$theme] = '';
302
			}
303
		}
304
305
		return Theming::$jsonThemePropsCache[$theme];
306
	}
307
308
	/**
309
	 * Normalizes all defined colors in a JSON theme to valid hex colors.
310
	 *
311
	 * @param array $themeProps A hash with the properties defined a theme.json file
312
	 */
313
	private static function normalizeColors($themeProps) {
314
		$colorKeys = [
315
			'primary-color',
316
			'primary-color:hover',
317
			'mainbar-text-color',
318
			'action-color',
319
			'action-color:hover',
320
			'selection-color',
321
			'selection-text-color',
322
			'focus-color',
323
		];
324
		foreach ($colorKeys as $ck) {
325
			$themeProps[$ck] = isset($themeProps[$ck]) ? Colors::getHexColorFromCssColor($themeProps[$ck]) : null;
326
		}
327
328
		return $themeProps;
329
	}
330
331
	/**
332
	 * Utility function to fix relative urls in JSON themes.
333
	 *
334
	 * @param string $url   the url to be fixed
335
	 * @param string $theme the name of the theme the url is part of
336
	 */
337
	private static function fixUrl($url, $theme) {
338
		// the url is absolute we don't have to fix anything
339
		if (preg_match('/^https?:\/\//', $url)) {
340
			return $url;
341
		}
342
343
		return PATH_PLUGIN_DIR . '/' . $theme . '/' . $url;
344
	}
345
346
	/**
347
	 * Retrieves the styles that should be added to the page for the json theme.
348
	 *
349
	 * @param string $theme The theme for which the properties should be retrieved
350
	 *
351
	 * @return string The styles (between <style> tags)
352
	 */
353
	public static function getStyles($theme) {
354
		$styles = '';
355
		if (!Theming::isJsonTheme($theme)) {
356
			$css = Theming::getCss($theme);
357
			foreach ($css as $file) {
0 ignored issues
show
Bug introduced by
The expression $css of type string is not traversable.
Loading history...
358
				$styles .= '<link rel="stylesheet" type="text/css" href="' . $file . '" />' . "\n";
359
			}
360
361
			return $styles;
362
		}
363
364
		// Convert the json theme to css styles
365
		$themeProps = Theming::getJsonThemeProps($theme);
366
		if (!$themeProps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $themeProps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
367
			return $styles;
368
		}
369
370
		$themeProps = Theming::normalizeColors($themeProps);
371
372
		if ($themeProps['primary-color']) {
373
			if (!$themeProps['primary-color:hover']) {
374
				[, , $l] = Colors::rgb2hsl(Colors::colorString2Object($themeProps['primary-color']));
375
				if ($l > 20) {
376
					$themeProps['primary-color:hover'] = Colors::darker($themeProps['primary-color'], 10);
377
				}
378
				else {
379
					$themeProps['primary-color:hover'] = Colors::lighter($themeProps['primary-color'], 20);
380
				}
381
			}
382
383
			if (!$themeProps['mainbar-text-color']) {
384
				// Check if the main bar is not too light for white text (i.e. the default color)
385
				if (Colors::getLuma($themeProps['primary-color']) > 155) {
386
					$themeProps['mainbar-text-color'] = '#000000';
387
				}
388
			}
389
390
			if (!$themeProps['selection-color']) {
391
				$themeProps['selection-color'] = Colors::setLuminance($themeProps['primary-color'], 80);
392
			}
393
		}
394
		if ($themeProps['action-color'] && !$themeProps['action-color:hover']) {
395
			$themeProps['action-color:hover'] = Colors::darker($themeProps['action-color'], 10);
396
		}
397
		if (isset($themeProps['selection-color']) && !isset($themeProps['selection-text-color'])) {
398
			// Set a text color for the selection-color
399
			$hsl = Colors::rgb2hsl($themeProps['selection-color']);
400
			if ($hsl['l'] > 50) {
401
				$hsl['l'] = 5;
402
			}
403
			else {
404
				$hsl['l'] = 95;
405
			}
406
			$themeProps['selection-text-color'] = Colors::colorObject2string(Colors::hsl2rgb($hsl));
407
		}
408
409
		if (isset($themeProps['background-image'])) {
410
			$themeProps['background-image'] = Theming::fixUrl($themeProps['background-image'], $theme);
411
		}
412
		if (isset($themeProps['logo-large'])) {
413
			$themeProps['logo-large'] = Theming::fixUrl($themeProps['logo-large'], $theme);
414
		}
415
		if (isset($themeProps['logo-small'])) {
416
			$themeProps['logo-small'] = Theming::fixUrl($themeProps['logo-small'], $theme);
417
		}
418
		if (isset($themeProps['logo-large']) && !isset($themeProps['logo-small'])) {
419
			$themeProps['logo-small'] = $themeProps['logo-large'];
420
		}
421
		if (isset($themeProps['spinner-image'])) {
422
			$themeProps['spinner-image'] = Theming::fixUrl($themeProps['spinner-image'], $theme);
423
		}
424
		$styles = '<style>';
425
		foreach ($themeProps as $k => $v) {
426
			if ($v && isset(Theming::$styles[$k])) {
427
				$styles .= str_replace("{{{$k}}}", htmlspecialchars((string) $v), Theming::$styles[$k]);
428
			}
429
		}
430
		$styles .= '</style>' . "\n";
431
432
		// Add the defined stylesheets
433
		if (isset($themeProps['stylesheets'])) {
434
			if (is_string($themeProps['stylesheets'])) {
435
				$stylesheets = explode(' ', $themeProps['stylesheets']);
436
			}
437
			elseif (is_array($themeProps['stylesheets'])) {
438
				$stylesheets = $themeProps['stylesheets'];
439
			}
440
			foreach ($stylesheets as $stylesheet) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $stylesheets does not seem to be defined for all execution paths leading up to this point.
Loading history...
441
				if (is_string($stylesheet)) {
442
					$stylesheet = trim($stylesheet);
443
					if (empty($stylesheet)) {
444
						continue;
445
					}
446
					$styles .= "\t\t" . '<link rel="stylesheet" type="text/css" href="' . htmlspecialchars((string) Theming::fixUrl($stylesheet, $theme)) . '" />' . "\n";
447
				}
448
			}
449
		}
450
451
		return $styles;
452
	}
453
454
	/**
455
	 * The templates of the styles that a json theme can add to the page.
456
	 *
457
	 * @property
458
	 */
459
	private static $styles = [
460
		'primary-color' => '
461
			/* The Sign in button of the login screen */
462
			body.login #form-container #submitbutton,
463
			#loading-mask #form-container #submitbutton {
464
				background: {{primary-color}};
465
			}
466
467
			/* The top bar of the Welcome dialog */
468
			.zarafa-welcome-body > .x-panel-bwrap > .x-panel-body div.zarafa-welcome-title {
469
				border-left: 1px solid {{primary-color}};
470
				border-right: 1px solid {{primary-color}};
471
				background: {{primary-color}};
472
			}
473
474
			/* The border line under the top menu bar */
475
			body #zarafa-mainmenu {
476
				border-color: {{primary-color}};
477
			}
478
			/* The background color of the top menu bar */
479
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct {
480
				background-color: {{primary-color}};
481
			}
482
			/* Unread items */
483
			.k-unreadborders .x-grid3-row.x-grid3-row-collapsed.mail_unread > table,
484
			.k-unreadborders .x-grid3-row.x-grid3-row-expanded.mail_unread > table {
485
		        	border-left: 4px solid {{primary-color}} !important;
486
			}
487
		',
488
489
		'primary-color:hover' => '
490
			/* Hover state and active state of the Sign in button */
491
			body.login #form-container #submitbutton:hover,
492
			#loading-mask #form-container #submitbutton:hover,
493
			body.login #form-container #submitbutton:active,
494
			#loading-mask #form-container #submitbutton:active {
495
				background: {{primary-color:hover}};
496
			}
497
498
			/* Background color of the hover state of the buttons in the top menu bar */
499
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn.x-btn-over,
500
			/* Background color of the active state of the buttons (i.e. when the buttons get clicked) */
501
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn.x-btn-over.x-btn-click,
502
			/* Background color of the selected button */
503
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active,
504
			/* Background color of the hover state of selected button */
505
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over,
506
			/* Background color of the active state of selected button */
507
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over.x-btn-click {
508
				background-color: {{primary-color:hover}} !important;
509
			}
510
		',
511
512
		'mainbar-text-color' => '
513
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct,
514
			/* Text color of the buttons in the top menu bar */
515
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn button.x-btn-text,
516
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn-over button.x-btn-text,
517
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn-over.x-btn-click button.x-btn-text,
518
			/* Text color of the selected button in the top menu bar */
519
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active button.x-btn-text,
520
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over button.x-btn-text,
521
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over.x-btn-click button.x-btn-text {
522
				color: {{mainbar-text-color}} !important;
523
			}
524
		',
525
526
		'action-color' => '
527
			/****************************************************************************
528
			 *  Action color
529
			 * ===============
530
			 * Some elements have a different color than the default color of these elements
531
			 * to get extra attention, e.g. "call-to-action buttons", the current day
532
			 * in the calendar, etc.
533
			 ****************************************************************************/
534
			/* Buttons, normal state */
535
			.x-btn.zarafa-action .x-btn-small,
536
			.x-btn.zarafa-action .x-btn-medium,
537
			.x-btn.zarafa-action .x-btn-large,
538
			/* Buttons, active state */
539
			.x-btn.zarafa-action.x-btn-over.x-btn-click .x-btn-small,
540
			.x-btn.zarafa-action.x-btn-over.x-btn-click .x-btn-medium,
541
			.x-btn.zarafa-action.x-btn-over.x-btn-click .x-btn-large,
542
			.x-btn.zarafa-action.x-btn-click .x-btn-small,
543
			.x-btn.zarafa-action.x-btn-click .x-btn-medium,
544
			.x-btn.zarafa-action.x-btn-click .x-btn-large,
545
			/* Special case: Popup, Windows, or Messageboxes (first button is by default styled as the action button) */
546
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
547
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
548
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
549
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
550
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
551
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
552
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
553
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
554
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
555
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
556
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
557
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
558
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
559
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
560
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
561
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
562
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
563
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
564
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
565
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
566
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
567
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
568
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
569
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
570
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
571
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
572
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
573
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
574
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
575
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
576
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
577
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
578
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
579
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
580
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
581
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
582
			/* action button in reminder popout */
583
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
584
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
585
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
586
			/* Current day in the calendar */
587
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current,
588
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current table,
589
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current table tr.x-freebusy-timeline-day td,
590
			/* The date pickers */
591
			.x-date-picker .x-date-inner td.x-date-today a,
592
			.x-date-picker .x-date-mp table td.x-date-mp-sel a,
593
			.x-date-picker .x-date-mp table tr.x-date-mp-btns td button.x-date-mp-ok {
594
				background: {{action-color}} !important;
595
			}
596
			/* Focused Action button */
597
			.x-btn.zarafa-action.x-btn-focus .x-btn-small, .x-btn.zarafa-action.x-btn-focus .x-btn-medium, .x-btn.zarafa-action.x-btn-focus .x-btn-large {
598
				background: {{action-color}} !important;
599
			}
600
			/* Selected calendar */
601
			.zarafa-calendar-tabarea-stroke.zarafa-calendar-tab-selected {
602
				border-top-color: {{action-color}};
603
			}
604
			.x-date-picker .x-date-inner td.x-date-weeknumber a,
605
			.zarafa-hierarchy-node-total-count span.zarafa-hierarchy-node-counter,
606
			.zarafa-hierarchy-node-unread-count span.zarafa-hierarchy-node-counter {
607
				color: {{action-color}};
608
			}
609
			.x-date-picker .x-date-inner td.x-date-today a {
610
				border-color: {{action-color}};
611
			}
612
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current,
613
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-body .x-freebusy-background .x-freebusy-timeline-day.x-freebusy-timeline-day-current {
614
				border-right-color: {{action-color}};
615
			}
616
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current table tr.x-freebusy-timeline-hour td:first-child,
617
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-body .x-freebusy-background .x-freebusy-timeline-day.x-freebusy-timeline-day-current td:first-child {
618
				border-left-color: {{action-color}};
619
			}
620
		',
621
622
		'action-color:hover' => '
623
			/* Buttons, hover state */
624
			.x-btn.zarafa-action.x-btn-over .x-btn-small,
625
			.x-btn.zarafa-action.x-btn-over .x-btn-medium,
626
			.x-btn.zarafa-action.x-btn-over .x-btn-large,
627
			/* Special case: Popup, Windows, or Messageboxes */
628
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
629
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
630
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
631
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
632
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
633
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
634
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
635
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
636
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
637
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
638
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
639
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
640
			/* action button in reminder popout */
641
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
642
			/* The date pickers */
643
			.x-date-picker .x-date-mp table tr.x-date-mp-btns td button.x-date-mp-ok:hover {
644
				background: {{action-color:hover}} !important;
645
			}
646
		',
647
648
		'selection-color' => '
649
			/*********************************************************************
650
			 * Selected items in grids and trees
651
			 * =================================
652
			 * The background color of the selected items in grids and trees can
653
			 * be changed to better suit the theme.
654
			 *********************************************************************/
655
			/* selected item in grids */
656
			.x-grid3-row.x-grid3-row-selected,
657
			.x-grid3 .x-grid3-row-selected .zarafa-grid-button-container,
658
			/* selected item in tree hierarchies */
659
			.x-tree-node .zarafa-hierarchy-node.x-tree-selected,
660
			/* selected items in boxfields (e.g. the recipient fields) */
661
			.x-zarafa-boxfield ul .x-zarafa-boxfield-item-focus,
662
			.x-zarafa-boxfield ul .x-zarafa-boxfield-recipient-item.x-zarafa-boxfield-item-focus,
663
			/* selected items in card view of Contacts context */
664
			div.zarafa-contact-cardview-selected,
665
			/* selected items in icon view of Notes context */
666
			.zarafa-note-iconview-selected,
667
			/* selected category in the Settings context */
668
			#zarafa-mainpanel-contentpanel-settings .zarafa-settings-category-panel .zarafa-settings-category-tab-active,
669
			/* selected date in date pickers */
670
			.x-date-picker .x-date-inner td.x-date-selected:not(.x-date-today) a,
671
			.x-date-picker .x-date-inner td.x-date-selected:not(.x-date-today) a:hover {
672
				background-color: {{selection-color}} !important;
673
				border-color: {{selection-color}};
674
			}
675
676
			/* Selected x-menu */
677
			.x-menu-item-selected {
678
			background-color: {{selection-color}};
679
			}
680
681
			/*********************************************************************
682
			 * Extra information about items
683
			 * =================================
684
			 * Sometimes extra information is shown in opened items. (e.g. "You replied
685
			 * to this message etc"). This can be styled with the following rules.
686
			 *********************************************************************/
687
			.preview-header-extrainfobox,
688
			.preview-header-extrainfobox-item,
689
			.k-appointmentcreatetab .zarafa-calendar-appointment-extrainfo div,
690
			.k-taskgeneraltab .zarafa-calendar-appointment-extrainfo div,
691
			.zarafa-mailcreatepanel > .x-panel-bwrap > .x-panel-body .zarafa-mailcreatepanel-extrainfo div {
692
				background: {{selection-color}} !important;
693
			}
694
695
			/* Selected mail item */
696
			.k-unreadborders .x-grid3-row.x-grid3-row-expanded.mail_read.x-grid3-row-selected > table {
697
			        border-left: 4px solid {{selection-color}} !important;
698
			}
699
700
			/* Hover selected item */
701
			.k-unreadborders .x-grid3-row.x-grid3-row-expanded.mail_read.x-grid3-row-selected.x-grid3-row-over > table,
702
			.k-unreadborders .x-grid3-row.x-grid3-row-collapsed.mail_read.x-grid3-row-selected > table {
703
			        border-left: 4px solid {{selection-color}} !important;
704
			}
705
		',
706
707
		'selection-text-color' => '
708
			/*********************************************************************
709
			 * Extra information about items
710
			 * =================================
711
			 * Sometimes extra information is shown in opened items. (e.g. "You replied
712
			 * to this message etc"). This can be styled with the following rules.
713
			 *********************************************************************/
714
			.preview-header-extrainfobox,
715
			.preview-header-extrainfobox-item,
716
			.k-appointmentcreatetab .zarafa-calendar-appointment-extrainfo div,
717
			.k-taskgeneraltab .zarafa-calendar-appointment-extrainfo div,
718
			.zarafa-mailcreatepanel > .x-panel-bwrap > .x-panel-body .zarafa-mailcreatepanel-extrainfo div {
719
				color: {{selection-text-color}};
720
			}
721
		',
722
723
		'focus-color' => '
724
			/*********************************************************************
725
			 * Focused items
726
			 * =================================
727
			 *********************************************************************/
728
			/* Normal button */
729
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) ~ .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-over):not(.x-btn-click) .x-btn-small,
730
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) ~ .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-over):not(.x-btn-click) .x-btn-small,
731
			.x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-click) .x-btn-small,
732
			.x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-click) .x-btn-medium,
733
			.x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-click) .x-btn-large,
734
			.x-toolbar .x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-noicon) .x-btn-small {
735
			border: 1px solid {{focus-color}} !important;
736
			}
737
			/* Login */
738
			body.login #form-container input:focus,
739
			#loading-mask #form-container input:focus {
740
			border-color: {{focus-color}};
741
			}
742
			input:focus {
743
				border-color: {{focus-color}};
744
			}
745
			/* Form elements */
746
			.x-form-text.x-form-focus:not(.x-trigger-noedit) {
747
				border-color: {{focus-color}} !important;
748
			}
749
			.x-form-field-wrap.x-trigger-wrap-focus:not(.x-freebusy-userlist-container) {
750
				border-color: {{focus-color}};
751
			}
752
			input.x-form-text.x-form-field.x-form-focus {
753
				border-color: {{focus-color}} !important;
754
			}
755
			.x-form-field-wrap.x-trigger-wrap-focus:not(.x-freebusy-userlist-container) input.x-form-text.x-form-field.x-form-focus {
756
			border-color: {{focus-color}} !important;
757
			}
758
		',
759
760
		'logo-large' => '
761
			/* The logo in the Login screen. Maximum size of the logo image is 220x60px. */
762
			body.login #form-container #logo,
763
			#loading-mask #form-container #logo {
764
				background: url({{logo-large}}) no-repeat right center;
765
				background-size: contain;
766
			}
767
		',
768
769
		'logo-small' => '
770
			/****************************************************************************
771
			 * The logo (shown on the right below the top bar)
772
			 * ===============================================
773
			 * The maximum height of the image that can be shown is 45px.
774
			 ****************************************************************************/
775
			.zarafa-maintoolbar {
776
				background-image: url({{logo-small}});
777
				background-size: auto 38px;
778
			}
779
		',
780
781
		'background-image' => '
782
			/*********************************************************************************************
783
			 * The Login screen and the Welcome screen
784
			 * =======================================
785
			 ********************************************************************************************/
786
			/* Background image of the login screen */
787
			body.login,
788
			#loading-mask,
789
			#bg,
790
			/* Background image of the Welcome screen */
791
			body.zarafa-welcome {
792
				background: url({{background-image}}) no-repeat center center;
793
				background-size: cover;
794
			}
795
		',
796
797
		'spinner-image' => '
798
			/* The spinner of the login/loading screen */
799
			body.login #form-container.loading .right,
800
			#loading-mask #form-container.loading .right {
801
				background: url({{spinner-image}}) no-repeat center center;
802
			}
803
		',
804
	];
805
}
806