diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 3b5bbe4190..5fe33b44ce 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -207,6 +207,19 @@ theme_settings: use_default: type: boolean label: 'Use default' + manifest: + type: mapping + label: 'Theme specific manifest data' + mapping: + orientation: + type: string + label: 'Orientation' + theme_color: + type: color_hex + label: 'Theme color' + background_color: + type: color_hex + label: 'Theme color' third_party_settings: type: sequence label: 'Third party settings' diff --git a/core/core.services.yml b/core/core.services.yml index 459503e44c..8e47a0a635 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1445,6 +1445,9 @@ services: - { name: needs_destruction } calls: - [setThemeManager, ['@theme.manager']] + theme.manifest_generator: + class: Drupal\Core\Theme\ManifestGenerator + arguments: ['@language_manager', '@config.factory', '@module_handler'] authentication: class: Drupal\Core\Authentication\AuthenticationManager arguments: ['@authentication_collector'] diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 68b10b3e42..be1226cd0e 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -487,6 +487,9 @@ function theme_settings_convert_to_config(array $theme_settings, Config $config) elseif (substr($key, 0, 7) == 'toggle_') { $config->set('features.' . Unicode::substr($key, 7), $value); } + elseif (substr($key, 0, 9) === 'manifest_') { + $config->set('manifest.' . Unicode::substr($key, 9), $value); + } elseif (!in_array($key, ['theme', 'logo_upload'])) { $config->set($key, $value); } diff --git a/core/lib/Drupal/Core/Theme/Manifest.php b/core/lib/Drupal/Core/Theme/Manifest.php new file mode 100644 index 0000000000..96cb4d9457 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/Manifest.php @@ -0,0 +1,54 @@ +data = $data; + } + + /** + * Generate a new instance of the manifest with updated data. + * + * @param array $data + * An array containing all data to generate the manifest file. + * + * @return static + */ + public function overwriteWithNewData($data) { + return new static($data); + } + + /** + * Returns the data as an array. + * + * @return array + * An array containing all data to generate the manifest file. + */ + public function toArray() { + return $this->data; + } + +} diff --git a/core/lib/Drupal/Core/Theme/ManifestGenerator.php b/core/lib/Drupal/Core/Theme/ManifestGenerator.php new file mode 100644 index 0000000000..0383e96923 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ManifestGenerator.php @@ -0,0 +1,121 @@ +currentLanguage = $languageManager->getCurrentLanguage(); + $this->configFactory = $configFactory; + $this->moduleHandler = $moduleHandler; + } + + /** + * Generate contents for the manifest.json for a specified theme. + * + * @param string $themeName + * The theme name. + * + * @return \Drupal\Core\Theme\Manifest + * The manifest file contents, complete with cache metadata. + */ + public function generateManifest($themeName) { + $this->themeConfig = $this->loadThemeConfig($themeName); + return $this->doGenerateManifest(); + } + + /** + * Generate the array of contents for the manifest.json file. + * + * @return \Drupal\Core\Theme\Manifest + * Data to generate the manifest file. + */ + protected function doGenerateManifest() { + $site_configuration = $this->configFactory->get('system.site'); + $data = [ + 'lang' => $this->currentLanguage->getId(), + 'dir' => $this->currentLanguage->getDirection(), + 'short_name' => $site_configuration->get('manifest.short_name'), + 'name' => $site_configuration->get('manifest.name'), + 'display' => $site_configuration->get('manifest.display'), + 'start_url' => $site_configuration->get('manifest.start_url'), + 'orientation' => $this->themeConfig->get('manifest.orientation'), + 'theme_color' => $this->themeConfig->get('manifest.theme_color'), + 'background_color' => $this->themeConfig->get('manifest.background_color'), + ]; + + $manifest_data = new Manifest($data); + $manifest_data->addCacheContexts(['languages', 'theme']); + $manifest_data->addCacheableDependency($site_configuration); + $manifest_data->addCacheableDependency($this->themeConfig); + + $this->moduleHandler->alter('manifest', $manifest_data); + + return $manifest_data; + } + + /** + * Load the theme configuration. + * + * @param string $themeName + * The theme name to generate data for. + * + * @return \Drupal\Core\Config\ImmutableConfig + * The configuration of the theme. + */ + protected function loadThemeConfig($themeName) { + $theme_specific_config = $this->configFactory->get($themeName . '.settings'); + if (isset($theme_specific_config->get()['manifest'])) { + return $theme_specific_config; + } + return $this->configFactory->get('system.theme.global'); + } + +} diff --git a/core/modules/color/color.module b/core/modules/color/color.module index f4ee9fd6e7..524e6c33d4 100644 --- a/core/modules/color/color.module +++ b/core/modules/color/color.module @@ -328,7 +328,7 @@ function color_palette_color_value($element, $input, FormStateInterface $form_st // necessary, falling back on the default value. $value = Textfield::valueCallback($element, $input, $form_state); $complete_form = $form_state->getCompleteForm(); - if (!$value || !isset($complete_form['#token']) || color_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state->getValue('form_token'), $complete_form['#token'])) { + if (!$value || !isset($complete_form['#token']) || system_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state->getValue('form_token'), $complete_form['#token'])) { return $value; } else { @@ -346,9 +346,13 @@ function color_palette_color_value($element, $input, FormStateInterface $form_st * @return bool * TRUE if the string is a valid hexadecimal CSS color string, or FALSE if it * isn't. + * + * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use + * system_valid_hexadecimal_string instead. */ function color_valid_hexadecimal_string($color) { - return preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color); + @trigger_error('color_valid_hexadecimal_string is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use system_valid_hexadecimal_string instead.', E_USER_DEPRECATED); + return system_valid_hexadecimal_string($color); } /** @@ -359,7 +363,7 @@ function color_valid_hexadecimal_string($color) { function color_scheme_form_validate($form, FormStateInterface $form_state) { // Only accept hexadecimal CSS color strings to avoid XSS upon use. foreach ($form_state->getValue('palette') as $key => $color) { - if (!color_valid_hexadecimal_string($color)) { + if (!system_valid_hexadecimal_string($color)) { $form_state->setErrorByName('palette][' . $key, t('You must enter a valid hexadecimal color value for %name.', ['%name' => $form['color']['palette'][$key]['#title']])); } } diff --git a/core/modules/system/config/install/system.site.yml b/core/modules/system/config/install/system.site.yml index d1ab8de7c6..dfa40d9792 100644 --- a/core/modules/system/config/install/system.site.yml +++ b/core/modules/system/config/install/system.site.yml @@ -10,3 +10,8 @@ admin_compact_mode: false weight_select_max: 100 langcode: en default_langcode: en +manifest: + start_url: '/' + display: 'browser' + short_name: 'Drupal' + name: '' diff --git a/core/modules/system/config/install/system.theme.global.yml b/core/modules/system/config/install/system.theme.global.yml index b8da4238c2..d861783e1c 100644 --- a/core/modules/system/config/install/system.theme.global.yml +++ b/core/modules/system/config/install/system.theme.global.yml @@ -12,3 +12,7 @@ logo: path: '' url: '' use_default: true +manifest: + orientation: 'portrait' + theme_color: '' + background_color: '' diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index a6f61b68ed..cbd3cf5415 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -44,6 +44,22 @@ system.site: mail_notification: type: string label: 'Notification email address' + manifest: + type: mapping + label: 'Site-wide manifest configuration' + mapping: + start_url: + type: path + label: 'Default start page' + display: + type: string + label: 'Display' + short_name: + type: label + label: 'Short name' + name: + type: label + label: 'Short name' system.maintenance: type: config_object diff --git a/core/modules/system/src/Controller/ManifestController.php b/core/modules/system/src/Controller/ManifestController.php new file mode 100644 index 0000000000..6e1eea8c93 --- /dev/null +++ b/core/modules/system/src/Controller/ManifestController.php @@ -0,0 +1,72 @@ +manifestGenerator = $manifestGenerator; + $this->themeManager = $themeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('theme.manifest_generator'), + $container->get('theme.manager') + ); + } + + /** + * Returns JSON data for the manifest.json file. + * + * The actual data is generated by \Drupal\Core\Theme\ManifestGenerator. + * + * @return \Drupal\Core\Cache\CacheableJsonResponse + * The JSON response. + */ + public function manifest() { + $themeName = $this->themeManager->getActiveTheme()->getName(); + $manifestData = $this->manifestGenerator->generateManifest($themeName); + + $response = new CacheableJsonResponse(); + $response->addCacheableDependency($manifestData); + $response->setData($manifestData->toArray()); + return $response; + } + +} diff --git a/core/modules/system/src/Form/SiteInformationForm.php b/core/modules/system/src/Form/SiteInformationForm.php index 4e4e1571fd..77bece61dd 100644 --- a/core/modules/system/src/Form/SiteInformationForm.php +++ b/core/modules/system/src/Form/SiteInformationForm.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Path\PathValidatorInterface; use Drupal\Core\Routing\RequestContext; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -93,6 +94,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { if (empty($site_mail)) { $site_mail = ini_get('sendmail_from'); } + $front_page = $site_config->get('page.front') != '/user/login' ? $this->aliasManager->getAliasByPath($site_config->get('page.front')) : ''; $form['site_information'] = [ '#type' => 'details', @@ -118,12 +120,51 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => t("The From address in automated emails sent during registration and new password requests, and other notifications. (Use an address ending in your site's domain to help prevent this email being flagged as spam.)"), '#required' => TRUE, ]; + $theme_config_url = Url::fromRoute('system.theme_settings')->toString(); + $form['site_information']['manifest'] = [ + '#type' => 'details', + '#title' => $this->t('Site manifest'), + '#description' => $this->t('Global values for the site manifest file. This is only part of the data needed for generating the manifest file, the rest of the configuration is theme specific', [':themeurl' => $theme_config_url]), + '#open' => FALSE, + ]; + $form['site_information']['manifest']['start_url'] = [ + '#type' => 'textfield', + '#title' => $this->t('Default front page'), + '#default_value' => $site_config->get('manifest.start_url') ?: $front_page, + '#size' => 40, + '#description' => $this->t('Optionally, specify a relative URL to display as the front page.'), + '#field_prefix' => $this->requestContext->getCompleteBaseUrl(), + ]; + $form['site_information']['manifest']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Name'), + '#description' => $this->t("The site name."), + '#size' => 50, + '#default_value' => $site_config->get('manifest.name') ?: $site_config->get('name'), + ]; + $form['site_information']['manifest']['short_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Short name'), + '#description' => $this->t("The short name of the site."), + '#size' => 12, + '#default_value' => $site_config->get('manifest.short_name') ?: $site_config->get('name'), + ]; + $form['site_information']['manifest']['site_display'] = [ + '#type' => 'select', + '#title' => $this->t('Display'), + '#options' => [ + 'fullscreen' => 'Fullscreen', + 'standalone' => 'Standalone', + 'minimal-ui' => 'Minimal UI', + 'browser' => 'Browser', + ], + '#default_value' => $site_config->get('manifest.display'), + ]; $form['front_page'] = [ '#type' => 'details', '#title' => t('Front page'), '#open' => TRUE, ]; - $front_page = $site_config->get('page.front') != '/user/login' ? $this->aliasManager->getAliasByPath($site_config->get('page.front')) : ''; $form['front_page']['site_frontpage'] = [ '#type' => 'textfield', '#title' => t('Default front page'), @@ -212,6 +253,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ->set('page.front', $form_state->getValue('site_frontpage')) ->set('page.403', $form_state->getValue('site_403')) ->set('page.404', $form_state->getValue('site_404')) + ->set('manifest.name', $form_state->getValue('name')) + ->set('manifest.short_name', $form_state->getValue('short_name')) + ->set('manifest.display', $form_state->getValue('site_display')) + ->set('manifest.start_url', $form_state->getValue('start_url')) ->save(); parent::submitForm($form, $form_state); diff --git a/core/modules/system/src/Form/ThemeSettingsForm.php b/core/modules/system/src/Form/ThemeSettingsForm.php index 03ded30b7d..68ab0d82a2 100644 --- a/core/modules/system/src/Form/ThemeSettingsForm.php +++ b/core/modules/system/src/Form/ThemeSettingsForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -367,6 +368,37 @@ public function buildForm(array $form, FormStateInterface $form_state, $theme = } } + $site_config_url = Url::fromRoute('system.site_information_settings')->toString(); + $form['manifest'] = [ + '#type' => 'details', + '#title' => $this->t('Theme specific manifest settings'), + '#description' => $this->t('Theme-specifc values for the site manifest file. This is only part of the data needed for generating the manifest file, the rest of the configuration is global to the site ', [':site_config' => $site_config_url]), + '#open' => FALSE, + ]; + $form['manifest']['manifest_orientation'] = [ + '#type' => 'select', + '#title' => t('Orientation'), + '#options' => [ + 'portrait' => t('Portrait'), + 'landscape' => t('Landscape'), + ], + '#default_value' => $this->config($config_key)->get('manifest.orientation'), + ]; + $form['manifest']['manifest_background_color'] = [ + '#type' => 'textfield', + '#title' => $this->t('Background color'), + '#description' => $this->t("Color of the background splash screen when launched from shortcut."), + '#size' => 8, + '#default_value' => $this->config($config_key)->get('manifest.background_color'), + ]; + $form['manifest']['manifest_theme_color'] = [ + '#type' => 'textfield', + '#title' => $this->t('Theme color'), + '#description' => $this->t("Color of browser bar when launched from shortcut."), + '#size' => 8, + '#default_value' => $this->config($config_key)->get('manifest.theme_color'), + ]; + return $form; } @@ -420,6 +452,14 @@ public function validateForm(array &$form, FormStateInterface $form_state) { } } } + + // Only accept hexadecimal CSS color strings to avoid XSS upon use. + $form_values = $form_state->getValues(); + foreach ($form_values as $key => $color) { + if (in_array($key, ['manifest_background_color', 'manifest_theme_color']) && !empty($color) && !system_valid_hexadecimal_string($color)) { + $form_state->setErrorByName('manifest][' . $key, t('You must enter a valid hexadecimal color value for %name.', ['%name' => $form['manifest'][$key]['#title']])); + } + } } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index e38e8ddce9..0c59ae99b4 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -675,6 +675,33 @@ function system_page_attachments(array &$page) { if (\Drupal::currentUser()->isAuthenticated()) { $page['#attached']['library'][] = 'core/drupal.active-link'; } + + $request = \Drupal::request(); + $is_non_admin = !\Drupal::service('router.admin_context')->isAdminRoute(); + $is_non_update = strpos($request->getScriptName(), 'update.php') === FALSE; + $is_non_install = strpos($request->getScriptName(), 'install.php') === FALSE; + if ($is_non_admin && $is_non_update && $is_non_install) { + $manifest_url = Url::fromRoute('system.manifest', [], ['query' => ['_format' => 'json']]); + $page['#attached']['html_head_link'][][] = [ + 'rel' => 'manifest', + 'href' => $manifest_url->toString(), + ]; + $manifest = \Drupal::service('theme.manifest_generator') + ->generateManifest(\Drupal::theme() + ->getActiveTheme() + ->getName()) + ->toArray(); + $page['#attached']['html_head'][] = [ + [ + '#tag' => 'meta', + '#attributes' => [ + 'name' => 'theme-color', + 'content' => $manifest['theme_color'], + ], + ], + 'theme-color', + ]; + } } /** @@ -799,6 +826,20 @@ function system_form_alter(&$form, FormStateInterface $form_state) { } } +/** + * Determines if a hexadecimal CSS color string is valid. + * + * @param string $color + * The string to check. + * + * @return bool + * TRUE if the string is a valid hexadecimal CSS color string, or FALSE if it + * isn't. + */ +function system_valid_hexadecimal_string($color) { + return preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color); +} + /** * Implements hook_form_FORM_ID_alter() for \Drupal\user\AccountForm. */ diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 9cfe2ca54c..dc9ebc2e3e 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -30,6 +30,14 @@ system.4xx: requirements: _access: 'TRUE' +system.manifest: + path: '/system/manifest' + defaults: + _controller: '\Drupal\system\Controller\ManifestController:manifest' + _title: 'Manifest' + requirements: + _access: 'TRUE' + system.admin: path: '/admin' defaults: diff --git a/core/modules/system/tests/modules/manifest_test/manifest_test.info.yml b/core/modules/system/tests/modules/manifest_test/manifest_test.info.yml new file mode 100644 index 0000000000..db64fffcd1 --- /dev/null +++ b/core/modules/system/tests/modules/manifest_test/manifest_test.info.yml @@ -0,0 +1,6 @@ +name: 'Manifest file test support' +type: module +description: 'Test hooks fired in manifest generation.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/manifest_test/manifest_test.module b/core/modules/system/tests/modules/manifest_test/manifest_test.module new file mode 100644 index 0000000000..a3e0283f87 --- /dev/null +++ b/core/modules/system/tests/modules/manifest_test/manifest_test.module @@ -0,0 +1,17 @@ +get('manifest_generation_test_link_alter', FALSE)) { + $new_data = $variables->toArray(); + $new_data['altered'] = 'Owl'; + $variables = $variables->overwriteWithNewData($new_data); + } +} diff --git a/core/modules/system/tests/src/Functional/Theme/ManifestTest.php b/core/modules/system/tests/src/Functional/Theme/ManifestTest.php new file mode 100644 index 0000000000..9e4f613454 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Theme/ManifestTest.php @@ -0,0 +1,68 @@ +drupalGet(''); + + // Check that the link to the manifest file is in the response. + $this->assertSession()->responseContains('system/manifest'); + + $content = $this->drupalGet('system/manifest'); + $manifest_content = json_decode($content, TRUE); + $this->assertNotEmpty($manifest_content); + $this->assertArrayHasKey('short_name', $manifest_content); + $this->assertSame('Drupal', $manifest_content['short_name']); + + $system_config = $this->container->get('config.factory')->getEditable('system.site'); + $system_config->set('manifest.short_name', 'Llama'); + $system_config->save(); + + $content = $this->drupalGet('system/manifest'); + $manifest_content = json_decode($content, TRUE); + $this->assertNotEmpty($manifest_content); + $this->assertArrayHasKey('short_name', $manifest_content); + $this->assertSame('Llama', $manifest_content['short_name']); + + // Check that the alter hook hasn't fired. + $this->assertArrayNotHasKey('altered', $manifest_content); + } + + /** + * Tests the alter hook for the manifest data. + */ + public function testManifestAlter() { + \Drupal::state()->set('manifest_generation_test_link_alter', TRUE); + + $content = $this->drupalGet('system/manifest'); + $manifest_content = json_decode($content, TRUE); + + $this->assertArrayHasKey('altered', $manifest_content); + $this->assertSame('Owl', $manifest_content['altered']); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php index 33c963de3c..e929448bc0 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php @@ -121,6 +121,12 @@ class MigrateSystemConfigurationTest extends MigrateDrupal6TestBase { // langcode and default_langcode are not handled by the migration. 'langcode' => 'en', 'default_langcode' => 'en', + 'manifest' => [ + 'start_url' => '/', + 'display' => 'browser', + 'short_name' => 'Drupal', + 'name' => '', + ], ], ]; diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php index 0216e2dd31..18eb148b0d 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php @@ -122,6 +122,12 @@ class MigrateSystemConfigurationTest extends MigrateDrupal7TestBase { // langcode and default_langcode are not handled by the migration. 'langcode' => 'en', 'default_langcode' => 'en', + 'manifest' => [ + 'start_url' => '/', + 'display' => 'browser', + 'short_name' => 'Drupal', + 'name' => '', + ], ], ]; diff --git a/core/modules/system/tests/src/Kernel/Theme/ManifestGeneratorTest.php b/core/modules/system/tests/src/Kernel/Theme/ManifestGeneratorTest.php new file mode 100644 index 0000000000..8ac2b1ee56 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Theme/ManifestGeneratorTest.php @@ -0,0 +1,74 @@ +installConfig(['system']); + + $this->config('system.site') + ->set('manifest.short_name', 'owl') + ->set('manifest.name', 'Micrathene whitneyi') + ->save(); + $this->config('system.theme.global') + ->set('manifest.theme_color', '#f00') + ->set('manifest.background_color', '#00f') + ->save(); + } + + /** + * Tests that the data for the manifest.json file gets generated correctly. + */ + public function testGenerateManifest() { + /** @var \Drupal\Core\Theme\ManifestGenerator $sut */ + $sut = $this->container->get('theme.manifest_generator'); + $manifest_object = $sut->generateManifest('bartik'); + $this->assertInstanceOf(Manifest::class, $manifest_object); + $data = $manifest_object->toArray(); + + // Language data is extracted as expected. + $this->assertArrayHasKey('lang', $data); + $this->assertArrayHasKey('dir', $data); + $this->assertSame('en', $data['lang']); + $this->assertSame('ltr', $data['dir']); + + // Site configuration. + $this->assertArrayHasKey('short_name', $data); + $this->assertArrayHasKey('name', $data); + $this->assertArrayHasKey('display', $data); + $this->assertArrayHasKey('start_url', $data); + $this->assertSame('owl', $data['short_name']); + $this->assertSame('Micrathene whitneyi', $data['name']); + $this->assertSame('browser', $data['display']); + $this->assertSame('/', $data['start_url']); + + // Theme configuration. + $this->assertArrayHasKey('orientation', $data); + $this->assertArrayHasKey('theme_color', $data); + $this->assertArrayHasKey('background_color', $data); + $this->assertSame('portrait', $data['orientation']); + $this->assertSame('#f00', $data['theme_color']); + $this->assertSame('#00f', $data['background_color']); + } + +} diff --git a/core/modules/system/tests/src/Unit/Theme/Drupal/Tests/Unit/ManifestTest.php b/core/modules/system/tests/src/Unit/Theme/Drupal/Tests/Unit/ManifestTest.php new file mode 100644 index 0000000000..057aba044c --- /dev/null +++ b/core/modules/system/tests/src/Unit/Theme/Drupal/Tests/Unit/ManifestTest.php @@ -0,0 +1,27 @@ + 'Can fly']; + $sut = new Manifest($data); + $this->assertEquals($data, $sut->toArray()); + + $new_data = ['llama' => 'Can not fly']; + $overwritten = $sut->overwriteWithNewData($new_data); + $this->assertEquals($data, $sut->toArray()); + $this->assertEquals($new_data, $overwritten->toArray()); + } + +} diff --git a/core/profiles/minimal/config/install/system.theme.global.yml b/core/profiles/minimal/config/install/system.theme.global.yml index 0fdc4b977e..e447ec5766 100644 --- a/core/profiles/minimal/config/install/system.theme.global.yml +++ b/core/profiles/minimal/config/install/system.theme.global.yml @@ -12,3 +12,7 @@ logo: path: '' url: '' use_default: true +manifest: + orientation: 'portrait' + theme_color: '' + background_color: ''