3 namespace Drupal\system\Form;
5 use Drupal\Core\Extension\ThemeHandlerInterface;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\Core\Render\Element;
8 use Drupal\Core\StreamWrapper\PublicStream;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10 use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
11 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
12 use Drupal\Core\Config\ConfigFactoryInterface;
13 use Drupal\Core\Extension\ModuleHandlerInterface;
14 use Drupal\Core\Form\ConfigFormBase;
15 use Drupal\Core\Theme\ThemeManagerInterface;
18 * Displays theme configuration for entire site and individual themes.
20 class ThemeSettingsForm extends ConfigFormBase {
25 * @var \Drupal\Core\Extension\ModuleHandlerInterface
27 protected $moduleHandler;
32 * @var \Drupal\Core\Extension\ThemeHandlerInterface
34 protected $themeHandler;
37 * The MIME type guesser.
39 * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
41 protected $mimeTypeGuesser;
44 * An array of configuration names that should be editable.
48 protected $editableConfig = [];
53 * @var \Drupal\Core\Theme\ThemeManagerInterface
55 protected $themeManager;
58 * Constructs a ThemeSettingsForm object.
60 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
61 * The factory for configuration objects.
62 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
63 * The module handler instance to use.
64 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
66 * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
67 * The MIME type guesser instance to use.
69 public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, MimeTypeGuesserInterface $mime_type_guesser, ThemeManagerInterface $theme_manager) {
70 parent::__construct($config_factory);
72 $this->moduleHandler = $module_handler;
73 $this->themeHandler = $theme_handler;
74 $this->mimeTypeGuesser = $mime_type_guesser;
75 $this->themeManager = $theme_manager;
81 public static function create(ContainerInterface $container) {
83 $container->get('config.factory'),
84 $container->get('module_handler'),
85 $container->get('theme_handler'),
86 $container->get('file.mime_type.guesser'),
87 $container->get('theme.manager')
94 public function getFormId() {
95 return 'system_theme_settings';
101 protected function getEditableConfigNames() {
102 return $this->editableConfig;
108 * @param string $theme
111 public function buildForm(array $form, FormStateInterface $form_state, $theme = '') {
112 $form = parent::buildForm($form, $form_state);
114 $themes = $this->themeHandler->listInfo();
116 // Default settings are defined in theme_get_setting() in includes/theme.inc
118 if (!$this->themeHandler->hasUi($theme)) {
119 throw new NotFoundHttpException();
121 $var = 'theme_' . $theme . '_settings';
122 $config_key = $theme . '.settings';
123 $themes = $this->themeHandler->listInfo();
124 $features = $themes[$theme]->info['features'];
127 $var = 'theme_settings';
128 $config_key = 'system.theme.global';
130 // @todo this is pretty meaningless since we're using theme_get_settings
131 // which means overrides can bleed into active config here. Will be fixed
132 // by https://www.drupal.org/node/2402467.
133 $this->editableConfig = [$config_key];
139 $form['config_key'] = [
141 '#value' => $config_key
146 'node_user_picture' => t('User pictures in posts'),
147 'comment_user_picture' => t('User pictures in comments'),
148 'comment_user_verification' => t('User verification status in comments'),
149 'favicon' => t('Shortcut icon'),
152 // Some features are not always available
154 if (!user_picture_enabled()) {
155 $disabled['toggle_node_user_picture'] = TRUE;
156 $disabled['toggle_comment_user_picture'] = TRUE;
158 if (!$this->moduleHandler->moduleExists('comment')) {
159 $disabled['toggle_comment_user_picture'] = TRUE;
160 $disabled['toggle_comment_user_verification'] = TRUE;
163 $form['theme_settings'] = [
164 '#type' => 'details',
165 '#title' => t('Page element display'),
168 foreach ($toggles as $name => $title) {
169 if ((!$theme) || in_array($name, $features)) {
170 $form['theme_settings']['toggle_' . $name] = ['#type' => 'checkbox', '#title' => $title, '#default_value' => theme_get_setting('features.' . $name, $theme)];
171 // Disable checkboxes for features not supported in the current configuration.
172 if (isset($disabled['toggle_' . $name])) {
173 $form['theme_settings']['toggle_' . $name]['#disabled'] = TRUE;
178 if (!Element::children($form['theme_settings'])) {
179 // If there is no element in the theme settings details then do not show
180 // it -- but keep it in the form if another module wants to alter.
181 $form['theme_settings']['#access'] = FALSE;
184 // Logo settings, only available when file.module is enabled.
185 if ((!$theme || in_array('logo', $features)) && $this->moduleHandler->moduleExists('file')) {
187 '#type' => 'details',
188 '#title' => t('Logo image'),
191 $form['logo']['default_logo'] = [
192 '#type' => 'checkbox',
193 '#title' => t('Use the logo supplied by the theme'),
194 '#default_value' => theme_get_setting('logo.use_default', $theme),
197 $form['logo']['settings'] = [
198 '#type' => 'container',
200 // Hide the logo settings when using the default logo.
202 'input[name="default_logo"]' => ['checked' => TRUE],
206 $form['logo']['settings']['logo_path'] = [
207 '#type' => 'textfield',
208 '#title' => t('Path to custom logo'),
209 '#default_value' => theme_get_setting('logo.path', $theme),
211 $form['logo']['settings']['logo_upload'] = [
213 '#title' => t('Upload logo image'),
215 '#description' => t("If you don't have direct file access to the server, use this field to upload your logo.")
219 if (((!$theme) || in_array('favicon', $features)) && $this->moduleHandler->moduleExists('file')) {
221 '#type' => 'details',
222 '#title' => t('Favicon'),
224 '#description' => t("Your shortcut icon, or favicon, is displayed in the address bar and bookmarks of most browsers."),
226 // Hide the shortcut icon settings fieldset when shortcut icon display
229 'input[name="toggle_favicon"]' => ['checked' => FALSE],
233 $form['favicon']['default_favicon'] = [
234 '#type' => 'checkbox',
235 '#title' => t('Use the favicon supplied by the theme'),
236 '#default_value' => theme_get_setting('favicon.use_default', $theme),
238 $form['favicon']['settings'] = [
239 '#type' => 'container',
241 // Hide the favicon settings when using the default favicon.
243 'input[name="default_favicon"]' => ['checked' => TRUE],
247 $form['favicon']['settings']['favicon_path'] = [
248 '#type' => 'textfield',
249 '#title' => t('Path to custom icon'),
250 '#default_value' => theme_get_setting('favicon.path', $theme),
252 $form['favicon']['settings']['favicon_upload'] = [
254 '#title' => t('Upload favicon image'),
255 '#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon.")
259 // Inject human-friendly values and form element descriptions for logo and
261 foreach (['logo' => 'logo.svg', 'favicon' => 'favicon.ico'] as $type => $default) {
262 if (isset($form[$type]['settings'][$type . '_path'])) {
263 $element = &$form[$type]['settings'][$type . '_path'];
265 // If path is a public:// URI, display the path relative to the files
266 // directory; stream wrappers are not end-user friendly.
267 $original_path = $element['#default_value'];
268 $friendly_path = NULL;
269 if (file_uri_scheme($original_path) == 'public') {
270 $friendly_path = file_uri_target($original_path);
271 $element['#default_value'] = $friendly_path;
274 // Prepare local file path for description.
275 if ($original_path && isset($friendly_path)) {
276 $local_file = strtr($original_path, ['public:/' => PublicStream::basePath()]);
279 $local_file = drupal_get_path('theme', $theme) . '/' . $default;
282 $local_file = $this->themeManager->getActiveTheme()->getPath() . '/' . $default;
285 $element['#description'] = t('Examples: <code>@implicit-public-file</code> (for a file in the public filesystem), <code>@explicit-file</code>, or <code>@local-file</code>.', [
286 '@implicit-public-file' => isset($friendly_path) ? $friendly_path : $default,
287 '@explicit-file' => file_uri_scheme($original_path) !== FALSE ? $original_path : 'public://' . $default,
288 '@local-file' => $local_file,
294 // Call engine-specific settings.
295 $function = $themes[$theme]->prefix . '_engine_settings';
296 if (function_exists($function)) {
297 $form['engine_specific'] = [
298 '#type' => 'details',
299 '#title' => t('Theme-engine-specific settings'),
301 '#description' => t('These settings only exist for the themes based on the %engine theme engine.', ['%engine' => $themes[$theme]->prefix]),
303 $function($form, $form_state);
306 // Create a list which includes the current theme and all its base themes.
307 if (isset($themes[$theme]->base_themes)) {
308 $theme_keys = array_keys($themes[$theme]->base_themes);
309 $theme_keys[] = $theme;
312 $theme_keys = [$theme];
315 // Save the name of the current theme (if any), so that we can temporarily
316 // override the current theme and allow theme_get_setting() to work
317 // without having to pass the theme name to it.
318 $default_active_theme = $this->themeManager->getActiveTheme();
319 $default_theme = $default_active_theme->getName();
320 /** @var \Drupal\Core\Theme\ThemeInitialization $theme_initialization */
321 $theme_initialization = \Drupal::service('theme.initialization');
322 $this->themeManager->setActiveTheme($theme_initialization->getActiveThemeByName($theme));
324 // Process the theme and all its base themes.
325 foreach ($theme_keys as $theme) {
326 // Include the theme-settings.php file.
327 $theme_path = drupal_get_path('theme', $theme);
328 $theme_settings_file = $theme_path . '/theme-settings.php';
329 $theme_file = $theme_path . '/' . $theme . '.theme';
330 $filenames = [$theme_settings_file, $theme_file];
331 foreach ($filenames as $filename) {
332 if (file_exists($filename)) {
333 require_once $filename;
335 // The file must be required for the cached form too.
336 $files = $form_state->getBuildInfo()['files'];
337 if (!in_array($filename, $files)) {
338 $files[] = $filename;
340 $form_state->addBuildInfo('files', $files);
344 // Call theme-specific settings.
345 $function = $theme . '_form_system_theme_settings_alter';
346 if (function_exists($function)) {
347 $function($form, $form_state);
351 // Restore the original current theme.
352 if (isset($default_theme)) {
353 $this->themeManager->setActiveTheme($default_active_theme);
356 $this->themeManager->resetActiveTheme();
366 public function validateForm(array &$form, FormStateInterface $form_state) {
367 parent::validateForm($form, $form_state);
369 if ($this->moduleHandler->moduleExists('file')) {
370 // Handle file uploads.
371 $validators = ['file_validate_is_image' => []];
373 // Check for a new uploaded logo.
374 $file = file_save_upload('logo_upload', $validators, FALSE, 0);
376 // File upload was attempted.
378 // Put the temporary file in form_values so we can save it on submit.
379 $form_state->setValue('logo_upload', $file);
382 // File upload failed.
383 $form_state->setErrorByName('logo_upload', $this->t('The logo could not be uploaded.'));
387 $validators = ['file_validate_extensions' => ['ico png gif jpg jpeg apng svg']];
389 // Check for a new uploaded favicon.
390 $file = file_save_upload('favicon_upload', $validators, FALSE, 0);
392 // File upload was attempted.
394 // Put the temporary file in form_values so we can save it on submit.
395 $form_state->setValue('favicon_upload', $file);
398 // File upload failed.
399 $form_state->setErrorByName('favicon_upload', $this->t('The favicon could not be uploaded.'));
403 // When intending to use the default logo, unset the logo_path.
404 if ($form_state->getValue('default_logo')) {
405 $form_state->unsetValue('logo_path');
408 // When intending to use the default favicon, unset the favicon_path.
409 if ($form_state->getValue('default_favicon')) {
410 $form_state->unsetValue('favicon_path');
413 // If the user provided a path for a logo or favicon file, make sure a file
414 // exists at that path.
415 if ($form_state->getValue('logo_path')) {
416 $path = $this->validatePath($form_state->getValue('logo_path'));
418 $form_state->setErrorByName('logo_path', $this->t('The custom logo path is invalid.'));
421 if ($form_state->getValue('favicon_path')) {
422 $path = $this->validatePath($form_state->getValue('favicon_path'));
424 $form_state->setErrorByName('favicon_path', $this->t('The custom favicon path is invalid.'));
433 public function submitForm(array &$form, FormStateInterface $form_state) {
434 parent::submitForm($form, $form_state);
436 $config_key = $form_state->getValue('config_key');
437 $this->editableConfig = [$config_key];
438 $config = $this->config($config_key);
440 // Exclude unnecessary elements before saving.
441 $form_state->cleanValues();
442 $form_state->unsetValue('var');
443 $form_state->unsetValue('config_key');
445 $values = $form_state->getValues();
447 // If the user uploaded a new logo or favicon, save it to a permanent location
448 // and use it in place of the default theme-provided file.
449 if (!empty($values['logo_upload'])) {
450 $filename = file_unmanaged_copy($values['logo_upload']->getFileUri());
451 $values['default_logo'] = 0;
452 $values['logo_path'] = $filename;
454 if (!empty($values['favicon_upload'])) {
455 $filename = file_unmanaged_copy($values['favicon_upload']->getFileUri());
456 $values['default_favicon'] = 0;
457 $values['favicon_path'] = $filename;
458 $values['toggle_favicon'] = 1;
460 unset($values['logo_upload']);
461 unset($values['favicon_upload']);
463 // If the user entered a path relative to the system files directory for
464 // a logo or favicon, store a public:// URI so the theme system can handle it.
465 if (!empty($values['logo_path'])) {
466 $values['logo_path'] = $this->validatePath($values['logo_path']);
468 if (!empty($values['favicon_path'])) {
469 $values['favicon_path'] = $this->validatePath($values['favicon_path']);
472 if (empty($values['default_favicon']) && !empty($values['favicon_path'])) {
473 $values['favicon_mimetype'] = $this->mimeTypeGuesser->guess($values['favicon_path']);
476 theme_settings_convert_to_config($values, $config)->save();
480 * Helper function for the system_theme_settings form.
482 * Attempts to validate normal system paths, paths relative to the public files
483 * directory, or stream wrapper URIs. If the given path is any of the above,
484 * returns a valid path or URI that the theme system can display.
486 * @param string $path
487 * A path relative to the Drupal root or to the public files directory, or
488 * a stream wrapper URI.
490 * A valid path that can be displayed through the theme system, or FALSE if
491 * the path could not be validated.
493 protected function validatePath($path) {
494 // Absolute local file paths are invalid.
495 if (drupal_realpath($path) == $path) {
498 // A path relative to the Drupal root or a fully qualified URI is valid.
499 if (is_file($path)) {
502 // Prepend 'public://' for relative file paths within public filesystem.
503 if (file_uri_scheme($path) === FALSE) {
504 $path = 'public://' . $path;
506 if (is_file($path)) {