Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / core / modules / system / src / Form / ThemeSettingsForm.php
1 <?php
2
3 namespace Drupal\system\Form;
4
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;
16
17 /**
18  * Displays theme configuration for entire site and individual themes.
19  *
20  * @internal
21  */
22 class ThemeSettingsForm extends ConfigFormBase {
23
24   /**
25    * The module handler.
26    *
27    * @var \Drupal\Core\Extension\ModuleHandlerInterface
28    */
29   protected $moduleHandler;
30
31   /**
32    * The theme handler.
33    *
34    * @var \Drupal\Core\Extension\ThemeHandlerInterface
35    */
36   protected $themeHandler;
37
38   /**
39    * The MIME type guesser.
40    *
41    * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
42    */
43   protected $mimeTypeGuesser;
44
45   /**
46    * An array of configuration names that should be editable.
47    *
48    * @var array
49    */
50   protected $editableConfig = [];
51
52   /**
53    * The theme manager.
54    *
55    * @var \Drupal\Core\Theme\ThemeManagerInterface
56    */
57   protected $themeManager;
58
59   /**
60    * Constructs a ThemeSettingsForm object.
61    *
62    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
63    *   The factory for configuration objects.
64    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
65    *   The module handler instance to use.
66    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
67    *   The theme handler.
68    * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
69    *   The MIME type guesser instance to use.
70    */
71   public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, MimeTypeGuesserInterface $mime_type_guesser, ThemeManagerInterface $theme_manager) {
72     parent::__construct($config_factory);
73
74     $this->moduleHandler = $module_handler;
75     $this->themeHandler = $theme_handler;
76     $this->mimeTypeGuesser = $mime_type_guesser;
77     $this->themeManager = $theme_manager;
78   }
79
80   /**
81    * {@inheritdoc}
82    */
83   public static function create(ContainerInterface $container) {
84     return new static(
85       $container->get('config.factory'),
86       $container->get('module_handler'),
87       $container->get('theme_handler'),
88       $container->get('file.mime_type.guesser'),
89       $container->get('theme.manager')
90     );
91   }
92
93   /**
94    * {@inheritdoc}
95    */
96   public function getFormId() {
97     return 'system_theme_settings';
98   }
99
100   /**
101    * {@inheritdoc}
102    */
103   protected function getEditableConfigNames() {
104     return $this->editableConfig;
105   }
106
107   /**
108    * {@inheritdoc}
109    *
110    * @param string $theme
111    *   The theme name.
112    */
113   public function buildForm(array $form, FormStateInterface $form_state, $theme = '') {
114     $form = parent::buildForm($form, $form_state);
115
116     $themes = $this->themeHandler->listInfo();
117
118     // Default settings are defined in theme_get_setting() in includes/theme.inc
119     if ($theme) {
120       if (!$this->themeHandler->hasUi($theme)) {
121         throw new NotFoundHttpException();
122       }
123       $var = 'theme_' . $theme . '_settings';
124       $config_key = $theme . '.settings';
125       $themes = $this->themeHandler->listInfo();
126       $features = $themes[$theme]->info['features'];
127     }
128     else {
129       $var = 'theme_settings';
130       $config_key = 'system.theme.global';
131     }
132     // @todo this is pretty meaningless since we're using theme_get_settings
133     //   which means overrides can bleed into active config here. Will be fixed
134     //   by https://www.drupal.org/node/2402467.
135     $this->editableConfig = [$config_key];
136
137     $form['var'] = [
138       '#type' => 'hidden',
139       '#value' => $var
140     ];
141     $form['config_key'] = [
142       '#type' => 'hidden',
143       '#value' => $config_key
144     ];
145
146     // Toggle settings
147     $toggles = [
148       'node_user_picture' => t('User pictures in posts'),
149       'comment_user_picture' => t('User pictures in comments'),
150       'comment_user_verification' => t('User verification status in comments'),
151       'favicon' => t('Shortcut icon'),
152     ];
153
154     // Some features are not always available
155     $disabled = [];
156     if (!user_picture_enabled()) {
157       $disabled['toggle_node_user_picture'] = TRUE;
158       $disabled['toggle_comment_user_picture'] = TRUE;
159     }
160     if (!$this->moduleHandler->moduleExists('comment')) {
161       $disabled['toggle_comment_user_picture'] = TRUE;
162       $disabled['toggle_comment_user_verification'] = TRUE;
163     }
164
165     $form['theme_settings'] = [
166       '#type' => 'details',
167       '#title' => t('Page element display'),
168       '#open' => TRUE,
169     ];
170     foreach ($toggles as $name => $title) {
171       if ((!$theme) || in_array($name, $features)) {
172         $form['theme_settings']['toggle_' . $name] = ['#type' => 'checkbox', '#title' => $title, '#default_value' => theme_get_setting('features.' . $name, $theme)];
173         // Disable checkboxes for features not supported in the current configuration.
174         if (isset($disabled['toggle_' . $name])) {
175           $form['theme_settings']['toggle_' . $name]['#disabled'] = TRUE;
176         }
177       }
178     }
179
180     if (!Element::children($form['theme_settings'])) {
181       // If there is no element in the theme settings details then do not show
182       // it -- but keep it in the form if another module wants to alter.
183       $form['theme_settings']['#access'] = FALSE;
184     }
185
186     // Logo settings, only available when file.module is enabled.
187     if ((!$theme || in_array('logo', $features)) && $this->moduleHandler->moduleExists('file')) {
188       $form['logo'] = [
189         '#type' => 'details',
190         '#title' => t('Logo image'),
191         '#open' => TRUE,
192       ];
193       $form['logo']['default_logo'] = [
194         '#type' => 'checkbox',
195         '#title' => t('Use the logo supplied by the theme'),
196         '#default_value' => theme_get_setting('logo.use_default', $theme),
197         '#tree' => FALSE,
198       ];
199       $form['logo']['settings'] = [
200         '#type' => 'container',
201         '#states' => [
202           // Hide the logo settings when using the default logo.
203           'invisible' => [
204             'input[name="default_logo"]' => ['checked' => TRUE],
205           ],
206         ],
207       ];
208       $form['logo']['settings']['logo_path'] = [
209         '#type' => 'textfield',
210         '#title' => t('Path to custom logo'),
211         '#default_value' => theme_get_setting('logo.path', $theme),
212       ];
213       $form['logo']['settings']['logo_upload'] = [
214         '#type' => 'file',
215         '#title' => t('Upload logo image'),
216         '#maxlength' => 40,
217         '#description' => t("If you don't have direct file access to the server, use this field to upload your logo."),
218         '#upload_validators' => [
219           'file_validate_is_image' => [],
220         ],
221       ];
222     }
223
224     if (((!$theme) || in_array('favicon', $features)) && $this->moduleHandler->moduleExists('file')) {
225       $form['favicon'] = [
226         '#type' => 'details',
227         '#title' => t('Favicon'),
228         '#open' => TRUE,
229         '#description' => t("Your shortcut icon, or favicon, is displayed in the address bar and bookmarks of most browsers."),
230         '#states' => [
231           // Hide the shortcut icon settings fieldset when shortcut icon display
232           // is disabled.
233           'invisible' => [
234             'input[name="toggle_favicon"]' => ['checked' => FALSE],
235           ],
236         ],
237       ];
238       $form['favicon']['default_favicon'] = [
239         '#type' => 'checkbox',
240         '#title' => t('Use the favicon supplied by the theme'),
241         '#default_value' => theme_get_setting('favicon.use_default', $theme),
242       ];
243       $form['favicon']['settings'] = [
244         '#type' => 'container',
245         '#states' => [
246           // Hide the favicon settings when using the default favicon.
247           'invisible' => [
248             'input[name="default_favicon"]' => ['checked' => TRUE],
249           ],
250         ],
251       ];
252       $form['favicon']['settings']['favicon_path'] = [
253         '#type' => 'textfield',
254         '#title' => t('Path to custom icon'),
255         '#default_value' => theme_get_setting('favicon.path', $theme),
256       ];
257       $form['favicon']['settings']['favicon_upload'] = [
258         '#type' => 'file',
259         '#title' => t('Upload favicon image'),
260         '#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon."),
261         '#upload_validators' => [
262           'file_validate_extensions' => [
263             'ico png gif jpg jpeg apng svg',
264           ],
265         ],
266       ];
267     }
268
269     // Inject human-friendly values and form element descriptions for logo and
270     // favicon.
271     foreach (['logo' => 'logo.svg', 'favicon' => 'favicon.ico'] as $type => $default) {
272       if (isset($form[$type]['settings'][$type . '_path'])) {
273         $element = &$form[$type]['settings'][$type . '_path'];
274
275         // If path is a public:// URI, display the path relative to the files
276         // directory; stream wrappers are not end-user friendly.
277         $original_path = $element['#default_value'];
278         $friendly_path = NULL;
279         if (file_uri_scheme($original_path) == 'public') {
280           $friendly_path = file_uri_target($original_path);
281           $element['#default_value'] = $friendly_path;
282         }
283
284         // Prepare local file path for description.
285         if ($original_path && isset($friendly_path)) {
286           $local_file = strtr($original_path, ['public:/' => PublicStream::basePath()]);
287         }
288         elseif ($theme) {
289           $local_file = drupal_get_path('theme', $theme) . '/' . $default;
290         }
291         else {
292           $local_file = $this->themeManager->getActiveTheme()->getPath() . '/' . $default;
293         }
294
295         $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>.', [
296           '@implicit-public-file' => isset($friendly_path) ? $friendly_path : $default,
297           '@explicit-file' => file_uri_scheme($original_path) !== FALSE ? $original_path : 'public://' . $default,
298           '@local-file' => $local_file,
299         ]);
300       }
301     }
302
303     if ($theme) {
304       // Call engine-specific settings.
305       $function = $themes[$theme]->prefix . '_engine_settings';
306       if (function_exists($function)) {
307         $form['engine_specific'] = [
308           '#type' => 'details',
309           '#title' => t('Theme-engine-specific settings'),
310           '#open' => TRUE,
311           '#description' => t('These settings only exist for the themes based on the %engine theme engine.', ['%engine' => $themes[$theme]->prefix]),
312         ];
313         $function($form, $form_state);
314       }
315
316       // Create a list which includes the current theme and all its base themes.
317       if (isset($themes[$theme]->base_themes)) {
318         $theme_keys = array_keys($themes[$theme]->base_themes);
319         $theme_keys[] = $theme;
320       }
321       else {
322         $theme_keys = [$theme];
323       }
324
325       // Save the name of the current theme (if any), so that we can temporarily
326       // override the current theme and allow theme_get_setting() to work
327       // without having to pass the theme name to it.
328       $default_active_theme = $this->themeManager->getActiveTheme();
329       $default_theme = $default_active_theme->getName();
330       /** @var \Drupal\Core\Theme\ThemeInitialization $theme_initialization */
331       $theme_initialization = \Drupal::service('theme.initialization');
332       $this->themeManager->setActiveTheme($theme_initialization->getActiveThemeByName($theme));
333
334       // Process the theme and all its base themes.
335       foreach ($theme_keys as $theme) {
336         // Include the theme-settings.php file.
337         $theme_path = drupal_get_path('theme', $theme);
338         $theme_settings_file = $theme_path . '/theme-settings.php';
339         $theme_file = $theme_path . '/' . $theme . '.theme';
340         $filenames = [$theme_settings_file, $theme_file];
341         foreach ($filenames as $filename) {
342           if (file_exists($filename)) {
343             require_once $filename;
344
345             // The file must be required for the cached form too.
346             $files = $form_state->getBuildInfo()['files'];
347             if (!in_array($filename, $files)) {
348               $files[] = $filename;
349             }
350             $form_state->addBuildInfo('files', $files);
351           }
352         }
353
354         // Call theme-specific settings.
355         $function = $theme . '_form_system_theme_settings_alter';
356         if (function_exists($function)) {
357           $function($form, $form_state);
358         }
359       }
360
361       // Restore the original current theme.
362       if (isset($default_theme)) {
363         $this->themeManager->setActiveTheme($default_active_theme);
364       }
365       else {
366         $this->themeManager->resetActiveTheme();
367       }
368     }
369
370     return $form;
371   }
372
373   /**
374    * {@inheritdoc}
375    */
376   public function validateForm(array &$form, FormStateInterface $form_state) {
377     parent::validateForm($form, $form_state);
378
379     if ($this->moduleHandler->moduleExists('file')) {
380       $validators = ['file_validate_is_image' => []];
381
382       // Check for a new uploaded logo.
383       $file = _file_save_upload_from_form($form['logo']['settings']['logo_upload'], $form_state, 0);
384       if ($file) {
385         // Put the temporary file in form_values so we can save it on submit.
386         $form_state->setValue('logo_upload', $file);
387       }
388
389       $validators = ['file_validate_extensions' => ['ico png gif jpg jpeg apng svg']];
390
391       // Check for a new uploaded favicon.
392       $file = _file_save_upload_from_form($form['favicon']['settings']['favicon_upload'], $form_state, 0);
393       if ($file) {
394         // Put the temporary file in form_values so we can save it on submit.
395         $form_state->setValue('favicon_upload', $file);
396       }
397
398       // When intending to use the default logo, unset the logo_path.
399       if ($form_state->getValue('default_logo')) {
400         $form_state->unsetValue('logo_path');
401       }
402
403       // When intending to use the default favicon, unset the favicon_path.
404       if ($form_state->getValue('default_favicon')) {
405         $form_state->unsetValue('favicon_path');
406       }
407
408       // If the user provided a path for a logo or favicon file, make sure a file
409       // exists at that path.
410       if ($form_state->getValue('logo_path')) {
411         $path = $this->validatePath($form_state->getValue('logo_path'));
412         if (!$path) {
413           $form_state->setErrorByName('logo_path', $this->t('The custom logo path is invalid.'));
414         }
415       }
416       if ($form_state->getValue('favicon_path')) {
417         $path = $this->validatePath($form_state->getValue('favicon_path'));
418         if (!$path) {
419           $form_state->setErrorByName('favicon_path', $this->t('The custom favicon path is invalid.'));
420         }
421       }
422     }
423   }
424
425   /**
426    * {@inheritdoc}
427    */
428   public function submitForm(array &$form, FormStateInterface $form_state) {
429     parent::submitForm($form, $form_state);
430
431     $config_key = $form_state->getValue('config_key');
432     $this->editableConfig = [$config_key];
433     $config = $this->config($config_key);
434
435     // Exclude unnecessary elements before saving.
436     $form_state->cleanValues();
437     $form_state->unsetValue('var');
438     $form_state->unsetValue('config_key');
439
440     $values = $form_state->getValues();
441
442     // If the user uploaded a new logo or favicon, save it to a permanent location
443     // and use it in place of the default theme-provided file.
444     if (!empty($values['logo_upload'])) {
445       $filename = file_unmanaged_copy($values['logo_upload']->getFileUri());
446       $values['default_logo'] = 0;
447       $values['logo_path'] = $filename;
448     }
449     if (!empty($values['favicon_upload'])) {
450       $filename = file_unmanaged_copy($values['favicon_upload']->getFileUri());
451       $values['default_favicon'] = 0;
452       $values['favicon_path'] = $filename;
453       $values['toggle_favicon'] = 1;
454     }
455     unset($values['logo_upload']);
456     unset($values['favicon_upload']);
457
458     // If the user entered a path relative to the system files directory for
459     // a logo or favicon, store a public:// URI so the theme system can handle it.
460     if (!empty($values['logo_path'])) {
461       $values['logo_path'] = $this->validatePath($values['logo_path']);
462     }
463     if (!empty($values['favicon_path'])) {
464       $values['favicon_path'] = $this->validatePath($values['favicon_path']);
465     }
466
467     if (empty($values['default_favicon']) && !empty($values['favicon_path'])) {
468       $values['favicon_mimetype'] = $this->mimeTypeGuesser->guess($values['favicon_path']);
469     }
470
471     theme_settings_convert_to_config($values, $config)->save();
472   }
473
474   /**
475    * Helper function for the system_theme_settings form.
476    *
477    * Attempts to validate normal system paths, paths relative to the public files
478    * directory, or stream wrapper URIs. If the given path is any of the above,
479    * returns a valid path or URI that the theme system can display.
480    *
481    * @param string $path
482    *   A path relative to the Drupal root or to the public files directory, or
483    *   a stream wrapper URI.
484    * @return mixed
485    *   A valid path that can be displayed through the theme system, or FALSE if
486    *   the path could not be validated.
487    */
488   protected function validatePath($path) {
489     // Absolute local file paths are invalid.
490     if (\Drupal::service('file_system')->realpath($path) == $path) {
491       return FALSE;
492     }
493     // A path relative to the Drupal root or a fully qualified URI is valid.
494     if (is_file($path)) {
495       return $path;
496     }
497     // Prepend 'public://' for relative file paths within public filesystem.
498     if (file_uri_scheme($path) === FALSE) {
499       $path = 'public://' . $path;
500     }
501     if (is_file($path)) {
502       return $path;
503     }
504     return FALSE;
505   }
506
507 }