'sort_name_format' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
'menubar_position' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
'menubar_color' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
+ 'theme_backend' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
+ 'theme_frontend' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
];
/**
$domain = ($translate === TRUE) ? $ext : $translate;
$this->addString($this->strings->get($domain, $this->getPath($ext, $file), 'text/javascript'), $domain);
}
- $this->resolveFileName($file, $ext);
- return $this->addScriptUrl($this->getUrl($ext, $file, TRUE), $weight, $region);
+ $url = $this->getUrl($ext, $this->filterMinify($ext, $file), TRUE);
+ return $this->addScriptUrl($url, $weight, $region);
}
/**
* @return CRM_Core_Resources
*/
public function addStyleFile($ext, $file, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
- $this->resolveFileName($file, $ext);
- return $this->addStyleUrl($this->getUrl($ext, $file, TRUE), $weight, $region);
+ /** @var Civi\Core\Themes $theme */
+ $theme = Civi::service('themes');
+ foreach ($theme->resolveUrls($theme->getActiveThemeKey(), $ext, $file) as $url) {
+ $this->addStyleUrl($url, $weight, $region);
+ }
+ return $this;
}
/**
}
/**
- * In debug mode, look for a non-minified version of this file
+ * Determine the minified file name.
*
- * @param string $fileName
- * @param string $extName
- */
- private function resolveFileName(&$fileName, $extName) {
- if (CRM_Core_Config::singleton()->debug && strpos($fileName, '.min.') !== FALSE) {
- $nonMiniFile = str_replace('.min.', '.', $fileName);
- if ($this->getPath($extName, $nonMiniFile)) {
- $fileName = $nonMiniFile;
+ * @param string $ext
+ * @param string $file
+ * @return string
+ * An updated $fileName. If a minified version exists and is supported by
+ * system policy, the minified version will be returned. Otherwise, the original.
+ */
+ public function filterMinify($ext, $file) {
+ if (CRM_Core_Config::singleton()->debug && strpos($file, '.min.') !== FALSE) {
+ $nonMiniFile = str_replace('.min.', '.', $file);
+ if ($this->getPath($ext, $nonMiniFile)) {
+ $file = $nonMiniFile;
}
}
+ return $file;
}
/**
);
}
+ /**
+ * A theme is a set of CSS files which are loaded on CiviCRM pages. To register a new
+ * theme, add it to the $themes array. Use these properties:
+ *
+ * - ext: string (required)
+ * The full name of the extension which defines the theme.
+ * Ex: "org.civicrm.themes.greenwich".
+ * - title: string (required)
+ * Visible title.
+ * - help: string (optional)
+ * Description of the theme's appearance.
+ * - url_callback: mixed (optional)
+ * A function ($themes, $themeKey, $cssExt, $cssFile) which returns the URL(s) for a CSS resource.
+ * Returns either an array of URLs or PASSTHRU.
+ * Ex: \Civi\Core\Themes\Resolvers::simple (default)
+ * Ex: \Civi\Core\Themes\Resolvers::none
+ * - prefix: string (optional)
+ * A prefix within the extension folder to prepend to the file name.
+ * - search_order: array (optional)
+ * A list of themes to search.
+ * Generally, the last theme should be "*fallback*" (Civi\Core\Themes::FALLBACK).
+ * - excludes: array (optional)
+ * A list of files (eg "civicrm:css/bootstrap.css" or "$ext:$file") which should never
+ * be returned (they are excluded from display).
+ *
+ * @param array $themes
+ * List of themes, keyed by name.
+ * @return null
+ * the return value is ignored
+ */
+ public static function themes(&$themes) {
+ return self::singleton()->invoke(1, $themes,
+ self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject,
+ 'civicrm_themes'
+ );
+ }
+
+ /**
+ * The activeTheme hook determines which theme is active.
+ *
+ * @param string $theme
+ * The identifier for the theme. Alterable.
+ * Ex: 'greenwich'.
+ * @param array $context
+ * Information about the current page-request. Includes some mix of:
+ * - page: the relative path of the current Civi page (Ex: 'civicrm/dashboard').
+ * - themes: an instance of the Civi\Core\Themes service.
+ * @return null
+ * the return value is ignored
+ */
+ public static function activeTheme(&$theme, $context) {
+ return self::singleton()->invoke(array('theme', 'context'), $theme, $context,
+ self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject,
+ 'civicrm_activeTheme'
+ );
+ }
+
/**
* This hook is called for declaring managed entities via API.
*
[]
));
+ $container->setDefinition('themes', new Definition(
+ 'Civi\Core\Themes',
+ []
+ ));
+
$container->setDefinition('pear_mail', new Definition('Mail'))
->setFactory('CRM_Utils_Mail::createMailer');
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.7 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2016 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Core;
+
+use Civi;
+
+/**
+ *
+ * @package CiviCRM_Hook
+ * @copyright CiviCRM LLC (c) 2004-2016
+ */
+class Themes {
+
+ /**
+ * The "default" theme adapts based on the latest recommendation from civicrm.org
+ * by switching to DEFAULT_THEME at runtime.
+ */
+ const DEFAULT_THEME = 'greenwich';
+
+ /**
+ * Fallback is a pseudotheme which can be included in "search_order".
+ * It locates files in the core/extension (non-theme) codebase.
+ */
+ const FALLBACK_THEME = '_fallback_';
+
+ const PASSTHRU = 'PASSTHRU';
+
+ /**
+ * @var string
+ * Ex: 'judy', 'liza'.
+ */
+ private $activeThemeKey = NULL;
+
+ /**
+ * @var array
+ * Array(string $themeKey => 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->userFrameworkFrontend ? '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_Array::value(\CRM_Core_Config::singleton()->userFrameworkURLVar, $_GET),
+ ]);
+
+ $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 isset($all[$themeKey]) ? $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 = array();
+ 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 array();
+ }
+
+ $cssId = $this->cssId($cssExt, $cssFile);
+
+ foreach ($all[$active]['search_order'] as $themeKey) {
+ if (isset($all[$themeKey]['excludes']) && in_array($cssId, $all[$themeKey]['excludes'])) {
+ $result = array();
+ }
+ 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";
+ }
+
+}
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.7 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2016 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Core\Themes;
+
+use Civi;
+
+/**
+ *
+ * @package CiviCRM_Hook
+ * @copyright CiviCRM LLC (c) 2004-2016
+ */
+class Resolvers {
+
+ /**
+ * In the simple format, the CSS file is loaded from the extension's "css" subdir;
+ * if it's missing, then it searches the parents.
+ *
+ * To use an alternate subdir, override "prefix".
+ *
+ * Simple themes may use the "search_order" to assimilate content from other themes.
+ *
+ * @param \Civi\Core\Themes $themes
+ * The theming subsystem.
+ * @param string $themeKey
+ * The active/desired theme key.
+ * @param string $cssExt
+ * The extension for which we want a themed CSS file (e.g. "civicrm").
+ * @param string $cssFile
+ * File name (e.g. "css/bootstrap.css").
+ * @return array|string
+ * List of CSS URLs, or PASSTHRU.
+ */
+ public static function simple($themes, $themeKey, $cssExt, $cssFile) {
+ $res = Civi::resources();
+ $theme = $themes->get($themeKey);
+ $file = '';
+ if (isset($theme['prefix'])) {
+ $file .= $theme['prefix'];
+ }
+ $file .= $themes->cssId($cssExt, $cssFile);
+ $file = $res->filterMinify($theme['ext'], $file);
+
+ if ($res->getPath($theme['ext'], $file)) {
+ return array($res->getUrl($theme['ext'], $file, TRUE));
+ }
+ else {
+ return Civi\Core\Themes::PASSTHRU;
+ }
+ }
+
+ /**
+ * The base handler falls back to loading files from the main application (rather than
+ * using the theme).
+ *
+ * @param \Civi\Core\Themes $themes
+ * The theming subsystem.
+ * @param string $themeKey
+ * The active/desired theme key.
+ * @param string $cssExt
+ * The extension for which we want a themed CSS file (e.g. "civicrm").
+ * @param string $cssFile
+ * File name (e.g. "css/bootstrap.css").
+ * @return array|string
+ * List of CSS URLs, or PASSTHRU.
+ */
+ public static function fallback($themes, $themeKey, $cssExt, $cssFile) {
+ $res = Civi::resources();
+ return array($res->getUrl($cssExt, $cssFile, TRUE));
+ }
+
+}
'description' => ts('Acceptable Mime Types that can be used as part of file urls'),
'help_text' => NULL,
],
+ 'theme_frontend' => [
+ 'group_name' => 'CiviCRM Preferences',
+ 'group' => 'core',
+ 'name' => 'theme_frontend',
+ 'type' => 'String',
+ 'quick_form_type' => 'Select',
+ 'html_type' => 'Select',
+ 'html_attributes' => array(
+ 'class' => 'crm-select2',
+ ),
+ 'pseudoconstant' => array(
+ 'callback' => 'call://themes/getAvailable',
+ ),
+ 'default' => 'default',
+ 'add' => '4.7',
+ 'title' => ts('Frontend Theme'),
+ 'is_domain' => 1,
+ 'is_contact' => 0,
+ 'description' => ts('Theme to use on frontend pages'),
+ 'help_text' => NULL,
+ ],
+ 'theme_backend' => [
+ 'group_name' => 'CiviCRM Preferences',
+ 'group' => 'core',
+ 'name' => 'theme_backend',
+ 'type' => 'String',
+ 'quick_form_type' => 'Select',
+ 'html_type' => 'Select',
+ 'html_attributes' => array(
+ 'class' => 'crm-select2',
+ ),
+ 'pseudoconstant' => array(
+ 'callback' => 'call://themes/getAvailable',
+ ),
+ 'default' => 'default',
+ 'add' => '4.7',
+ 'title' => ts('Backend Theme'),
+ 'is_domain' => 1,
+ 'is_contact' => 0,
+ 'description' => ts('Theme to use on backend pages'),
+ 'help_text' => NULL,
+ ],
];
{capture assign=invoiceURL}{crmURL p='civicrm/admin/setting/preferences/contribute' q="reset=1"}{/capture}
{ts 1=$invoiceURL}In order to enable logged in users to download invoices and credit notes from the dashboard, please first enable CiviCRM invoicing functionality <a href='%1'>Administer > CiviContribute > CiviContribute Component Settings</a>{/ts}
{/htxt}
+
+{* ***** Theme options ***** *}
+
+{capture assign=themeDefn}
+ <p>
+ {ts}The theme system allows you to change CiviCRM's appearance by replacing important CSS files.{/ts}
+ </p>
+{/capture}
+{capture assign=themeAdv}
+ {* TODO: Update when there is a page to link to:
+ <p>
+ {ts 1='http://fixme.example.com'}For more advanced theming options, consult the <a href="%1">theme documentation</a>.{/ts}
+ </p>
+ *}
+{/capture}
+
+{htxt id="theme-title"}
+ {ts}Theme{/ts}
+{/htxt}
+{htxt id="theme"}
+ {$themeDefn}
+ {$themeAdv}
+{/htxt}
+
+{htxt id="theme_backend-title"}
+ {ts}Backend Theme{/ts}
+{/htxt}
+{htxt id="theme_backend"}
+ {$themeDefn}
+ <p>
+ {ts}The backend theme determines the appearance on administrative screens, such as the "Manage Event" screen.{/ts}
+ </p>
+ {$themeAdv}
+{/htxt}
+
+{htxt id="theme_frontend-title"}
+ {ts}Frontend Theme{/ts}
+{/htxt}
+{htxt id="theme_frontend"}
+ {$themeDefn}
+ <p>
+ {ts}On WordPress, Joomla, or a similar CMS, the frontend theme determines the appearance on user-facing screens, such as the "Event Registration" screen.{/ts}
+ </p>
+ {$themeAdv}
+{/htxt}
{$form.menubar_color.html}
</td>
</tr>
+
+ {if $config->userSystem->is_drupal EQ '1'}
+ <tr class="crm-preferences-display-form-block-theme">
+ <td class="label">{ts}Theme{/ts} {help id="theme"}</td>
+ <td>{$form.theme_backend.html}</td>
+ </tr>
+ {else}
+ <tr class="crm-preferences-display-form-block-theme_backend">
+ <td class="label">{$form.theme_backend.label} {help id="theme_backend"}</td>
+ <td>{$form.theme_backend.html}</td>
+ </tr>
+ <tr class="crm-preferences-display-form-block-theme_frontend">
+ <td class="label">{$form.theme_frontend.label} {help id="theme_frontend"}</td>
+ <td>{$form.theme_frontend.html}</td>
+ </tr>
+ {/if}
</table>
<div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="bottom"}</div>
</div>
--- /dev/null
+/* Placeholder */
\ No newline at end of file
--- /dev/null
+/* Placeholder */
\ No newline at end of file
--- /dev/null
+<extension key='test.extension.uitest' type='module'>
+ <file>uitest</file>
+ <name>test_extension_uitest</name>
+</extension>
--- /dev/null
+/* Placeholder: judy/css/bootstrap.css */
\ No newline at end of file
--- /dev/null
+/* Placeholder: judy/css/civicrm.css */
\ No newline at end of file
--- /dev/null
+/* Placeholder: liz/css/bootstrap.css */
\ No newline at end of file
--- /dev/null
+/* Placeholder: liza/css/civicrm.css */
\ No newline at end of file
--- /dev/null
+/* Placeholder: liza/css/civicrm.min.css */
\ No newline at end of file
--- /dev/null
+/* Placeholder for liza/org.example.foobar:files/foo.css */
\ No newline at end of file
--- /dev/null
+<?php
+
+namespace Civi\Core;
+
+/**
+ * Class CRM_Core_RegionTest
+ *
+ * @group headless
+ */
+class ThemesTest extends \CiviUnitTestCase {
+
+ protected function setUp() {
+ $this->useTransaction();
+ parent::setUp();
+ }
+
+ public function getThemeExamples() {
+ $cases = [];
+
+ // --- Library of example themes which we can include in tests. ---
+
+ $hookJudy = [
+ 'judy' => [
+ 'title' => 'Judy Garland',
+ 'ext' => 'civicrm',
+ 'prefix' => 'tests/phpunit/Civi/Core/Theme/judy/',
+ 'excludes' => ['test.extension.uitest-files/ignoreme.css'],
+ ],
+ ];
+ $hookLiza = [
+ 'liza' => [
+ 'title' => 'Liza Minnelli',
+ 'prefix' => 'tests/phpunit/Civi/Core/Theme/liza/',
+ 'ext' => 'civicrm',
+ ],
+ ];
+ $hookBlueMarine = [
+ 'bluemarine' => [
+ 'title' => 'Blue Marine',
+ 'url_callback' => [__CLASS__, 'fakeCallback'],
+ 'ext' => 'civicrm',
+ ],
+ ];
+ $hookAquaMarine = [
+ 'aquamarine' => [
+ 'title' => 'Aqua Marine',
+ 'url_callback' => [__CLASS__, 'fakeCallback'],
+ 'ext' => 'civicrm',
+ 'search_order' => ['aquamarine', 'bluemarine', '_fallback_'],
+ ],
+ ];
+
+ $civicrmBaseUrl = "";
+
+ // --- Library of tests ---
+
+ // Use the default theme, Greenwich.
+ $cases[] = [
+ [],
+ 'default',
+ 'Greenwich',
+ [
+ 'civicrm-css/civicrm.css' => ["$civicrmBaseUrl/css/civicrm.css"],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ["/tests/extensions/test.extension.uitest/files/foo.css"],
+ ],
+ ];
+
+ // judy is defined. Let's use judy.
+ $cases[] = [
+ // Example hook data
+ $hookJudy,
+ 'judy',
+ // Example theme to inspect
+ 'Judy Garland',
+ [
+ 'civicrm-css/civicrm.css' => ["$civicrmBaseUrl/tests/phpunit/Civi/Core/Theme/judy/css/civicrm.css"],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ["/tests/extensions/test.extension.uitest/files/foo.css"],
+ // excluded
+ 'test.extension.uitest-files/ignoreme.css' => [],
+ ],
+ ];
+
+ // Misconfiguration: liza was previously used but then disappeared. Fallback to default, Greenwich.
+ $cases[] = [
+ $hookJudy,
+ 'liza',
+ 'Greenwich',
+ [
+ 'civicrm-css/civicrm.css' => ["$civicrmBaseUrl/css/civicrm.css"],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ["/tests/extensions/test.extension.uitest/files/foo.css"],
+ ],
+ ];
+
+ // We have some themes available, but the admin opted out.
+ $cases[] = [
+ $hookJudy,
+ 'none',
+ 'None (Unstyled)',
+ [
+ 'civicrm-css/civicrm.css' => [],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ["/tests/extensions/test.extension.uitest/files/foo.css"],
+ ],
+ ];
+
+ // Theme which overrides an extension's CSS file.
+ $cases[] = [
+ $hookJudy + $hookLiza,
+ 'liza',
+ 'Liza Minnelli',
+ [
+ // Warning: If your local system has overrides for the `debug_enabled`, these results may vary.
+ 'civicrm-css/civicrm.css' => ["$civicrmBaseUrl/tests/phpunit/Civi/Core/Theme/liza/css/civicrm.css"],
+ 'civicrm-css/civicrm.min.css' => ["$civicrmBaseUrl/tests/phpunit/Civi/Core/Theme/liza/css/civicrm.min.css"],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ["/tests/phpunit/Civi/Core/Theme/liza/test.extension.uitest-files/foo.css"],
+ ],
+ ];
+
+ // Theme has a custom URL-lookup function.
+ $cases[] = [
+ $hookBlueMarine + $hookAquaMarine,
+ 'bluemarine',
+ 'Blue Marine',
+ [
+ 'civicrm-css/civicrm.css' => ['http://example.com/blue/civicrm.css'],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ['http://example.com/blue/foobar/foo.css'],
+ ],
+ ];
+
+ // Theme is derived from another.
+ $cases[] = [
+ $hookBlueMarine + $hookAquaMarine,
+ 'aquamarine',
+ 'Aqua Marine',
+ [
+ 'civicrm-css/civicrm.css' => ['http://example.com/aqua/civicrm.css'],
+ 'civicrm-css/joomla.css' => ["$civicrmBaseUrl/css/joomla.css"],
+ 'test.extension.uitest-files/foo.css' => ['http://example.com/blue/foobar/foo.css'],
+ ],
+ ];
+
+ return $cases;
+ }
+
+ /**
+ * Test theme.
+ *
+ * @param array $inputtedHook
+ * @param string $themeKey
+ * @param string $expectedTitle
+ * @param array $expectedUrls
+ * List of files to lookup plus the expected URLs.
+ * Array("{$extName}-{$fileName}" => "{$expectUrl}").
+ *
+ * @dataProvider getThemeExamples
+ */
+ public function testTheme($inputtedHook, $themeKey, $expectedTitle, $expectedUrls) {
+ $this->hookClass->setHook('civicrm_themes', function (&$themes) use ($inputtedHook) {
+ foreach ($inputtedHook as $key => $value) {
+ $themes[$key] = $value;
+ }
+ });
+
+ \Civi::settings()->set('theme_frontend', $themeKey);
+ \Civi::settings()->set('theme_backend', $themeKey);
+
+ /** @var \Civi\Core\Themes $themeSvc */
+ $themeSvc = \Civi::service('themes');
+ $theme = $themeSvc->get($themeSvc->getActiveThemeKey());
+ if ($expectedTitle) {
+ $this->assertEquals($expectedTitle, $theme['title']);
+ }
+
+ foreach ($expectedUrls as $inputFile => $expectedUrl) {
+ list ($ext, $file) = explode('-', $inputFile, 2);
+ $actualUrl = $themeSvc->resolveUrls($themeSvc->getActiveThemeKey(), $ext, $file);
+ foreach (array_keys($actualUrl) as $k) {
+ // Ignore cache revision key (`?r=abcd1234`).
+ list ($actualUrl[$k]) = explode('?', $actualUrl[$k], 2);
+ }
+ $this->assertEquals($expectedUrl, $actualUrl, "Check URL for $inputFile");
+ }
+ }
+
+ public static function fakeCallback($themes, $themeKey, $cssExt, $cssFile) {
+ $map['bluemarine']['civicrm']['css/bootstrap.css'] = ['http://example.com/blue/bootstrap.css'];
+ $map['bluemarine']['civicrm']['css/civicrm.css'] = ['http://example.com/blue/civicrm.css'];
+ $map['bluemarine']['test.extension.uitest']['files/foo.css'] = ['http://example.com/blue/foobar/foo.css'];
+ $map['aquamarine']['civicrm']['css/civicrm.css'] = ['http://example.com/aqua/civicrm.css'];
+ return isset($map[$themeKey][$cssExt][$cssFile]) ? $map[$themeKey][$cssExt][$cssFile] : Themes::PASSTHRU;
+ }
+
+ public function testGetAll() {
+ $all = \Civi::service('themes')->getAll();
+ $this->assertTrue(isset($all['greenwich']));
+ $this->assertTrue(isset($all['_fallback_']));
+ }
+
+ public function testGetAvailable() {
+ $all = \Civi::service('themes')->getAvailable();
+ $this->assertTrue(isset($all['greenwich']));
+ $this->assertFalse(isset($all['_fallback_']));
+ }
+
+ public function testApiOptions() {
+ $result = $this->callAPISuccess('Setting', 'getoptions', [
+ 'field' => 'theme_backend',
+ ]);
+ $this->assertTrue(isset($result['values']['greenwich']));
+ $this->assertFalse(isset($result['values']['_fallback_']));
+ }
+
+}