diff --git a/README.md b/README.md index 9a910e2..7618002 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,55 @@ In some Browsers the SubsiteID is visible if you hover over the "Edit" link in t ### Subsite-specific themes -Download a second theme from http://www.silverstripe.com/themes/ and put it in your themes folder. Open admin/subsites?flush=1 and select one of your subsites from the menu on the bottom-left. You should see a Theme dropdown in the subsite details, and it should list both your original theme and the new theme. Select the new theme in the dropdown. Now, this subsite will use a different theme from the main site. +Download a second theme from http://www.silverstripe.com/themes/ and put it in your themes folder. Open +admin/subsites?flush=1 and select one of your subsites from the menu on the bottom-left. You should see a +Theme dropdown in the subsite details, and it should list both your original theme and the new theme. Select the new +theme in the dropdown. Now, this subsite will use a different theme from the main site. + +#### Cascading themes + +In SilverStripe 4 themes will resolve theme files by looking through a list of themes (see the documentation on +[creating your own theme](https://docs.silverstripe.org/en/4/developer_guides/templates/themes/#developing-your-own-theme)). +Subsites will inherit this configuration for the order of themes. Choosing a theme for a Subsite will set the list of +themes to that chosen theme, and all themes that are defined below the chosen theme in priority. For example, with a +theme configuration as follows: + +```yaml +SilverStripe\View\SSViewer: + themes: + - '$public' + - 'my-theme' + - 'watea' + - 'starter' + - '$default' +``` + +Choosing `watea` in your Subsite will create a cascading config as follows: + +```yaml +themes: + - 'watea' + - '$public' + - 'starter' + - '$default' +``` + +You may also completely define your own cascading theme lists for CMS users to choose as theme options for their +subsite: + +```yaml +SilverStripe\Subsites\Service\ThemeResolver: + theme_options: + normal: + - '$public' + - 'watea' + - 'starter' + - '$default' + special: + - 'my-theme' + - 'starter' + - '$default' +``` ### Limit available themes for a subsite diff --git a/src/Extensions/SiteTreeSubsites.php b/src/Extensions/SiteTreeSubsites.php index ed79f98..2ac5048 100644 --- a/src/Extensions/SiteTreeSubsites.php +++ b/src/Extensions/SiteTreeSubsites.php @@ -26,6 +26,7 @@ use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\Subsites\Model\Subsite; +use SilverStripe\Subsites\Service\ThemeResolver; use SilverStripe\Subsites\State\SubsiteState; use SilverStripe\View\SSViewer; @@ -390,10 +391,11 @@ class SiteTreeSubsites extends DataExtension */ public static function contentcontrollerInit($controller) { + /** @var Subsite $subsite */ $subsite = Subsite::currentSubsite(); if ($subsite && $subsite->Theme) { - SSViewer::add_themes([$subsite->Theme]); + SSViewer::set_themes(ThemeResolver::singleton()->getThemeList($subsite)); } if ($subsite && i18n::getData()->validate($subsite->Language)) { diff --git a/src/Model/Subsite.php b/src/Model/Subsite.php index 9f88643..e33e7ec 100644 --- a/src/Model/Subsite.php +++ b/src/Model/Subsite.php @@ -28,6 +28,7 @@ use SilverStripe\Security\Group; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; +use SilverStripe\Subsites\Service\ThemeResolver; use SilverStripe\Subsites\State\SubsiteState; use SilverStripe\Versioned\Versioned; use UnexpectedValueException; @@ -182,7 +183,6 @@ class Subsite extends DataObject /** * Gets the subsite currently set in the session. * - * @uses ControllerSubsites->controllerAugmentInit() * @return DataObject The current Subsite */ public static function currentSubsite() @@ -787,7 +787,7 @@ class Subsite extends DataObject */ public function allowedThemes() { - if ($themes = self::$allowed_themes) { + if (($themes = self::$allowed_themes) || ($themes = ThemeResolver::singleton()->getCustomThemeOptions())) { return ArrayLib::valuekey($themes); } @@ -874,7 +874,7 @@ class Subsite extends DataObject } // If there are no objects, default to the current hostname - return $_SERVER['HTTP_HOST']; + return Director::host(); } /** diff --git a/src/Model/SubsiteDomain.php b/src/Model/SubsiteDomain.php index 83f33b5..c21848c 100644 --- a/src/Model/SubsiteDomain.php +++ b/src/Model/SubsiteDomain.php @@ -186,7 +186,7 @@ class SubsiteDomain extends DataObject */ public function getSubstitutedDomain() { - $currentHost = $_SERVER['HTTP_HOST']; + $currentHost = Director::host(); // If there are wildcards in the primary domain (not recommended), make some // educated guesses about what to replace them with: diff --git a/src/Service/ThemeResolver.php b/src/Service/ThemeResolver.php new file mode 100644 index 0000000..297a2bc --- /dev/null +++ b/src/Service/ThemeResolver.php @@ -0,0 +1,99 @@ + [ + * '$public', + * 'starter', + * '$default', + * ], + * 'theme-2' => [ + * 'custom', + * 'watea', + * 'starter', + * '$public', + * '$default', + * ] + * ] + * + * @config + * @var null|array[] + */ + private static $theme_options; + + /** + * Get the list of themes for the given sub site that can be given to SSViewer::set_themes + * + * @param Subsite $site + * @return array + */ + public function getThemeList(Subsite $site) + { + $themes = array_values(SSViewer::get_themes()); + $siteTheme = $site->Theme; + + if (!$siteTheme) { + return $themes; + } + + $customOptions = $this->config()->get('theme_options'); + if ($customOptions && isset($customOptions[$siteTheme])) { + return $customOptions[$siteTheme]; + } + + // Ensure themes don't cascade "up" the list + $index = array_search($siteTheme, $themes); + + if ($index > 0) { + // 4.0 didn't have support for themes in the public webroot + $constant = SSViewer::class . '::PUBLIC_THEME'; + $publicConstantDefined = defined($constant); + + // Check if the default is public themes + $publicDefault = $publicConstantDefined && $themes[0] === SSViewer::PUBLIC_THEME; + + // Take only those that appear after theme chosen (non-inclusive) + $themes = array_slice($themes, $index + 1); + + // Add back in public + if ($publicDefault) { + array_unshift($themes, SSViewer::PUBLIC_THEME); + } + } + + // Add our theme + array_unshift($themes, $siteTheme); + + return $themes; + } + + /** + * Get a list of custom cascading theme definitions if available + * + * @return null|array + */ + public function getCustomThemeOptions() + { + $config = $this->config()->get('theme_options'); + + if (!$config) { + return null; + } + + return array_keys($config); + } +} diff --git a/tests/php/Service/ThemeResolverTest.php b/tests/php/Service/ThemeResolverTest.php new file mode 100644 index 0000000..0b98f3b --- /dev/null +++ b/tests/php/Service/ThemeResolverTest.php @@ -0,0 +1,143 @@ +set(SSViewer::class, 'themes', $this->themeList); + } + + public function testSubsiteWithoutThemeReturnsDefaultThemeList() + { + $subsite = new Subsite(); + $resolver = new ThemeResolver(); + + $this->assertSame($this->themeList, $resolver->getThemeList($subsite)); + } + + public function testSubsiteWithCustomThemePrependsToList() + { + $subsite = new Subsite(); + $subsite->Theme = 'subsite'; + + $resolver = new ThemeResolver(); + + $expected = array_merge(['subsite'], $this->themeList); + + $this->assertSame($expected, $resolver->getThemeList($subsite)); + } + + public function testSubsiteWithCustomThemeDoesNotCascadeUpTheList() + { + $subsite = new Subsite(); + $subsite->Theme = 'main'; + + $resolver = new ThemeResolver(); + + $expected = [ + 'main', // 'main' is moved to the top + '$public', // $public is preserved + // Anything above 'main' is removed + 'backup', + SSViewer::DEFAULT_THEME, + ]; + + $this->assertSame($expected, $resolver->getThemeList($subsite)); + } + + /** + * @dataProvider customThemeDefinitionsAreRespectedProvider + */ + public function testCustomThemeDefinitionsAreRespected($themeOptions, $siteTheme, $expected) + { + Config::modify()->set(ThemeResolver::class, 'theme_options', $themeOptions); + + $subsite = new Subsite(); + $subsite->Theme = $siteTheme; + + $resolver = new ThemeResolver(); + + $this->assertSame($expected, $resolver->getThemeList($subsite)); + } + + public function customThemeDefinitionsAreRespectedProvider() + { + return [ + // Simple + [ + ['test' => $expected = [ + 'subsite', + 'backup', + '$public', + SSViewer::DEFAULT_THEME, + ]], + 'test', + $expected + ], + // Many options + [ + [ + 'aye' => [ + 'aye', + 'thing', + SSViewer::DEFAULT_THEME, + ], + 'bee' => $expected = [ + 'subsite', + 'backup', + '$public', + SSViewer::DEFAULT_THEME, + ], + 'sea' => [ + 'mer', + 'ocean', + SSViewer::DEFAULT_THEME, + ], + ], + 'bee', + $expected + ], + // Conflicting with root definitions + [ + ['main' => $expected = [ + 'subsite', + 'backup', + '$public', + SSViewer::DEFAULT_THEME, + ]], + 'main', + $expected + ], + // Declaring a theme specifically should still work + [ + ['test' => [ + 'subsite', + 'backup', + '$public', + SSViewer::DEFAULT_THEME, + ]], + 'other', + array_merge(['other'], $this->themeList) + ], + ]; + } +} diff --git a/tests/php/SiteTreeSubsitesTest.php b/tests/php/SiteTreeSubsitesTest.php index dc54afa..9adb321 100644 --- a/tests/php/SiteTreeSubsitesTest.php +++ b/tests/php/SiteTreeSubsitesTest.php @@ -9,6 +9,8 @@ use SilverStripe\CMS\Forms\SiteTreeURLSegmentField; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Director; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Convert; +use SilverStripe\Core\Injector\Injector; use SilverStripe\ErrorPage\ErrorPage; use SilverStripe\Forms\FieldList; use SilverStripe\Security\Member; @@ -16,6 +18,7 @@ use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\Subsites\Extensions\SiteTreeSubsites; use SilverStripe\Subsites\Model\Subsite; use SilverStripe\Subsites\Pages\SubsitesVirtualPage; +use SilverStripe\Subsites\Service\ThemeResolver; use SilverStripe\Subsites\Tests\SiteTreeSubsitesTest\TestClassA; use SilverStripe\Subsites\Tests\SiteTreeSubsitesTest\TestClassB; use SilverStripe\Subsites\Tests\SiteTreeSubsitesTest\TestErrorPage; @@ -399,41 +402,25 @@ class SiteTreeSubsitesTest extends BaseSubsiteTest ]; } - public function testIfSubsiteThemeIsSetToThemeList() + public function testThemeResolverIsUsedForSettingThemeList() { - $defaultThemes = ['default']; - SSViewer::set_themes($defaultThemes); + $firstResolver = $this->createMock(ThemeResolver::class); + $firstResolver->expects($this->never())->method('getThemeList'); + Injector::inst()->registerService($firstResolver, ThemeResolver::class); $subsitePage = $this->objFromFixture(Page::class, 'home'); Subsite::changeSubsite($subsitePage->SubsiteID); $controller = ModelAsController::controller_for($subsitePage); SiteTree::singleton()->extend('contentcontrollerInit', $controller); - $this->assertEquals( - SSViewer::get_themes(), - $defaultThemes, - 'Themes should not be modified when Subsite has no theme defined' - ); + $secondResolver = $this->createMock(ThemeResolver::class); + $secondResolver->expects($this->once())->method('getThemeList'); + Injector::inst()->registerService($secondResolver, ThemeResolver::class); - $pageWithTheme = $this->objFromFixture(Page::class, 'subsite1_home'); - Subsite::changeSubsite($pageWithTheme->SubsiteID); - $controller = ModelAsController::controller_for($pageWithTheme); + $subsitePage = $this->objFromFixture(Page::class, 'subsite1_home'); + Subsite::changeSubsite($subsitePage->SubsiteID); + $controller = ModelAsController::controller_for($subsitePage); SiteTree::singleton()->extend('contentcontrollerInit', $controller); - $subsiteTheme = $pageWithTheme->Subsite()->Theme; - - $allThemes = SSViewer::get_themes(); - - $this->assertContains( - $subsiteTheme, - $allThemes, - 'Themes should be modified when Subsite has theme defined' - ); - - $this->assertEquals( - $subsiteTheme, - array_shift($allThemes), - 'Subsite theme should be prepeded to theme list' - ); } public function provideAlternateAbsoluteLink()