array $themeSpec). */ private $themes = NULL; /** * @var \CRM_Utils_Cache_Interface */ private $cache = NULL; /** * Theme constructor. * @param \CRM_Utils_Cache_Interface $cache */ public function __construct($cache = NULL) { $this->cache = $cache ? $cache : Civi::cache('long'); } /** * Determine the name of active theme. * * @return string * Ex: "greenwich". */ public function getActiveThemeKey() { if ($this->activeThemeKey === NULL) { // Ambivalent: is it better to use $config->userFrameworkFrontend or $template->get('urlIsPublic')? $config = \CRM_Core_Config::singleton(); $settingKey = $config->userSystem->isFrontEndPage() ? 'theme_frontend' : 'theme_backend'; $themeKey = Civi::settings()->get($settingKey); if ($themeKey === 'default') { $themeKey = self::DEFAULT_THEME; } \CRM_Utils_Hook::activeTheme($themeKey, [ 'themes' => $this, 'page' => \CRM_Utils_System::currentPath(), ]); $themes = $this->getAll(); $this->activeThemeKey = isset($themes[$themeKey]) ? $themeKey : self::DEFAULT_THEME; } return $this->activeThemeKey; } /** * Get the definition of the theme. * * @param string $themeKey * Ex: 'greenwich', 'shoreditch'. * @return array|NULL * @see CRM_Utils_Hook::themes */ public function get($themeKey) { $all = $this->getAll(); return $all[$themeKey] ?? NULL; } /** * Get a list of all known themes, including hidden base themes. * * @return array * List of themes, keyed by name. Same format as CRM_Utils_Hook::themes(), * but any default values are filled in. * @see CRM_Utils_Hook::themes */ public function getAll() { if ($this->themes === NULL) { // Cache includes URLs/paths, which change with runtime. $cacheKey = 'theme_list_' . \CRM_Core_Config_Runtime::getId(); $this->themes = $this->cache->get($cacheKey); if ($this->themes === NULL) { $this->themes = $this->buildAll(); $this->cache->set($cacheKey, $this->themes); } } return $this->themes; } /** * Get a list of available themes, excluding hidden base themes. * * This is the same as getAll(), but abstract themes like "_fallback_" * or "_newyork_base_" are omitted. * * @return array * List of themes. * Ex: ['greenwich' => 'Greenwich', 'shoreditch' => 'Shoreditch']. * @see CRM_Utils_Hook::themes */ public function getAvailable() { $result = []; foreach ($this->getAll() as $key => $theme) { if ($key[0] !== '_') { $result[$key] = $theme['title']; } } return $result; } /** * Get the URL(s) for a themed CSS file. * * This implements a prioritized search, in order: * - Check for the specified theme. * - If that doesn't exist, check for the default theme. * - If that doesn't exist, use the 'none' theme. * * @param string $active * Active theme key. * Ex: 'greenwich'. * @param string $cssExt * Ex: 'civicrm'. * @param string $cssFile * Ex: 'css/bootstrap.css' or 'css/civicrm.css'. * @return array * List of URLs to display. * Ex: array(string $url) */ public function resolveUrls($active, $cssExt, $cssFile) { $all = $this->getAll(); if (!isset($all[$active])) { return []; } $cssId = $this->cssId($cssExt, $cssFile); foreach ($all[$active]['search_order'] as $themeKey) { if (isset($all[$themeKey]['excludes']) && in_array($cssId, $all[$themeKey]['excludes'])) { $result = []; } else { $result = Civi\Core\Resolver::singleton() ->call($all[$themeKey]['url_callback'], array($this, $themeKey, $cssExt, $cssFile)); } if ($result !== self::PASSTHRU) { return $result; } } throw new \RuntimeException("Failed to resolve URL. Theme metadata may be incomplete."); } /** * Construct the list of available themes. * * @return array * List of themes, keyed by name. * @see CRM_Utils_Hook::themes */ protected function buildAll() { $themes = array( 'default' => array( 'ext' => 'civicrm', 'title' => ts('Automatic'), 'help' => ts('Determine a system default automatically'), // This is an alias. url_callback, search_order don't matter. ), 'greenwich' => array( 'ext' => 'civicrm', 'title' => 'Greenwich', 'help' => ts('CiviCRM 4.x look-and-feel'), ), 'none' => array( 'ext' => 'civicrm', 'title' => ts('None (Unstyled)'), 'help' => ts('Disable CiviCRM\'s built-in CSS files.'), 'search_order' => array('none', self::FALLBACK_THEME), 'excludes' => array( "css/civicrm.css", "css/bootstrap.css", ), ), self::FALLBACK_THEME => array( 'ext' => 'civicrm', 'title' => 'Fallback (Abstract Base Theme)', 'url_callback' => '\Civi\Core\Themes\Resolvers::fallback', 'search_order' => array(self::FALLBACK_THEME), ), ); \CRM_Utils_Hook::themes($themes); foreach (array_keys($themes) as $themeKey) { $themes[$themeKey] = $this->build($themeKey, $themes[$themeKey]); } return $themes; } /** * Apply defaults for a given them. * * @param string $themeKey * The name of the theme. Ex: 'greenwich'. * @param array $theme * The original theme definition of the theme (per CRM_Utils_Hook::themes). * @return array * The full theme definition of the theme (per CRM_Utils_Hook::themes). * @see CRM_Utils_Hook::themes */ protected function build($themeKey, $theme) { $defaults = array( 'name' => $themeKey, 'url_callback' => '\Civi\Core\Themes\Resolvers::simple', 'search_order' => array($themeKey, self::FALLBACK_THEME), ); $theme = array_merge($defaults, $theme); return $theme; } /** * @param string $cssExt * @param string $cssFile * @return string */ public function cssId($cssExt, $cssFile) { return ($cssExt === 'civicrm') ? $cssFile : "$cssExt-$cssFile"; } }