2ad98931a3e6377ca51c772b0f04c277feb15f4c
[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 class ThemeSettingsForm extends ConfigFormBase {
21
22   /**
23    * The module handler.
24    *
25    * @var \Drupal\Core\Extension\ModuleHandlerInterface
26    */
27   protected $moduleHandler;
28
29   /**
30    * The theme handler.
31    *
32    * @var \Drupal\Core\Extension\ThemeHandlerInterface
33    */
34   protected $themeHandler;
35
36   /**
37    * The MIME type guesser.
38    *
39    * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
40    */
41   protected $mimeTypeGuesser;
42
43   /**
44    * An array of configuration names that should be editable.
45    *
46    * @var array
47    */
48   protected $editableConfig = [];
49
50   /**
51    * The theme manager.
52    *
53    * @var \Drupal\Core\Theme\ThemeManagerInterface
54    */
55   protected $themeManager;
56
57   /**
58    * Constructs a ThemeSettingsForm object.
59    *
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
65    *   The theme handler.
66    * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
67    *   The MIME type guesser instance to use.
68    */
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);
71
72     $this->moduleHandler = $module_handler;
73     $this->themeHandler = $theme_handler;
74     $this->mimeTypeGuesser = $mime_type_guesser;
75     $this->themeManager = $theme_manager;
76   }
77
78   /**
79    * {@inheritdoc}
80    */
81   public static function create(ContainerInterface $container) {
82     return new static(
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')
88     );
89   }
90
91   /**
92    * {@inheritdoc}
93    */
94   public function getFormId() {
95     return 'system_theme_settings';
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   protected function getEditableConfigNames() {
102     return $this->editableConfig;
103   }
104
105   /**
106    * {@inheritdoc}
107    *
108    * @param string $theme
109    *   The theme name.
110    */
111   public function buildForm(array $form, FormStateInterface $form_state, $theme = '') {
112     $form = parent::buildForm($form, $form_state);
113
114     $themes = $this->themeHandler->listInfo();
115
116     // Default settings are defined in theme_get_setting() in includes/theme.inc
117     if ($theme) {
118       if (!$this->themeHandler->hasUi($theme)) {
119         throw new NotFoundHttpException();
120       }
121       $var = 'theme_' . $theme . '_settings';
122       $config_key = $theme . '.settings';
123       $themes = $this->themeHandler->listInfo();
124       $features = $themes[$theme]->info['features'];
125     }
126     else {
127       $var = 'theme_settings';
128       $config_key = 'system.theme.global';
129     }
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];
134
135     $form['var'] = [
136       '#type' => 'hidden',
137       '#value' => $var
138     ];
139     $form['config_key'] = [
140       '#type' => 'hidden',
141       '#value' => $config_key
142     ];
143
144     // Toggle settings
145     $toggles = [
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'),
150     ];
151
152     // Some features are not always available
153     $disabled = [];
154     if (!user_picture_enabled()) {
155       $disabled['toggle_node_user_picture'] = TRUE;
156       $disabled['toggle_comment_user_picture'] = TRUE;
157     }
158     if (!$this->moduleHandler->moduleExists('comment')) {
159       $disabled['toggle_comment_user_picture'] = TRUE;
160       $disabled['toggle_comment_user_verification'] = TRUE;
161     }
162
163     $form['theme_settings'] = [
164       '#type' => 'details',
165       '#title' => t('Page element display'),
166       '#open' => TRUE,
167     ];
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;
174         }
175       }
176     }
177
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;
182     }
183
184     // Logo settings, only available when file.module is enabled.
185     if ((!$theme || in_array('logo', $features)) && $this->moduleHandler->moduleExists('file')) {
186       $form['logo'] = [
187         '#type' => 'details',
188         '#title' => t('Logo image'),
189         '#open' => TRUE,
190       ];
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),
195         '#tree' => FALSE,
196       ];
197       $form['logo']['settings'] = [
198         '#type' => 'container',
199         '#states' => [
200           // Hide the logo settings when using the default logo.
201           'invisible' => [
202             'input[name="default_logo"]' => ['checked' => TRUE],
203           ],
204         ],
205       ];
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),
210       ];
211       $form['logo']['settings']['logo_upload'] = [
212         '#type' => 'file',
213         '#title' => t('Upload logo image'),
214         '#maxlength' => 40,
215         '#description' => t("If you don't have direct file access to the server, use this field to upload your logo.")
216       ];
217     }
218
219     if (((!$theme) || in_array('favicon', $features)) && $this->moduleHandler->moduleExists('file')) {
220       $form['favicon'] = [
221         '#type' => 'details',
222         '#title' => t('Favicon'),
223         '#open' => TRUE,
224         '#description' => t("Your shortcut icon, or favicon, is displayed in the address bar and bookmarks of most browsers."),
225         '#states' => [
226           // Hide the shortcut icon settings fieldset when shortcut icon display
227           // is disabled.
228           'invisible' => [
229             'input[name="toggle_favicon"]' => ['checked' => FALSE],
230           ],
231         ],
232       ];
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),
237       ];
238       $form['favicon']['settings'] = [
239         '#type' => 'container',
240         '#states' => [
241           // Hide the favicon settings when using the default favicon.
242           'invisible' => [
243             'input[name="default_favicon"]' => ['checked' => TRUE],
244           ],
245         ],
246       ];
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),
251       ];
252       $form['favicon']['settings']['favicon_upload'] = [
253         '#type' => 'file',
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.")
256       ];
257     }
258
259     // Inject human-friendly values and form element descriptions for logo and
260     // favicon.
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'];
264
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;
272         }
273
274         // Prepare local file path for description.
275         if ($original_path && isset($friendly_path)) {
276           $local_file = strtr($original_path, ['public:/' => PublicStream::basePath()]);
277         }
278         elseif ($theme) {
279           $local_file = drupal_get_path('theme', $theme) . '/' . $default;
280         }
281         else {
282           $local_file = $this->themeManager->getActiveTheme()->getPath() . '/' . $default;
283         }
284
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,
289         ]);
290       }
291     }
292
293     if ($theme) {
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'),
300           '#open' => TRUE,
301           '#description' => t('These settings only exist for the themes based on the %engine theme engine.', ['%engine' => $themes[$theme]->prefix]),
302         ];
303         $function($form, $form_state);
304       }
305
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;
310       }
311       else {
312         $theme_keys = [$theme];
313       }
314
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));
323
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;
334
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;
339             }
340             $form_state->addBuildInfo('files', $files);
341           }
342         }
343
344         // Call theme-specific settings.
345         $function = $theme . '_form_system_theme_settings_alter';
346         if (function_exists($function)) {
347           $function($form, $form_state);
348         }
349       }
350
351       // Restore the original current theme.
352       if (isset($default_theme)) {
353         $this->themeManager->setActiveTheme($default_active_theme);
354       }
355       else {
356         $this->themeManager->resetActiveTheme();
357       }
358     }
359
360     return $form;
361   }
362
363   /**
364    * {@inheritdoc}
365    */
366   public function validateForm(array &$form, FormStateInterface $form_state) {
367     parent::validateForm($form, $form_state);
368
369     if ($this->moduleHandler->moduleExists('file')) {
370       // Handle file uploads.
371       $validators = ['file_validate_is_image' => []];
372
373       // Check for a new uploaded logo.
374       $file = file_save_upload('logo_upload', $validators, FALSE, 0);
375       if (isset($file)) {
376         // File upload was attempted.
377         if ($file) {
378           // Put the temporary file in form_values so we can save it on submit.
379           $form_state->setValue('logo_upload', $file);
380         }
381         else {
382           // File upload failed.
383           $form_state->setErrorByName('logo_upload', $this->t('The logo could not be uploaded.'));
384         }
385       }
386
387       $validators = ['file_validate_extensions' => ['ico png gif jpg jpeg apng svg']];
388
389       // Check for a new uploaded favicon.
390       $file = file_save_upload('favicon_upload', $validators, FALSE, 0);
391       if (isset($file)) {
392         // File upload was attempted.
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         else {
398           // File upload failed.
399           $form_state->setErrorByName('favicon_upload', $this->t('The favicon could not be uploaded.'));
400         }
401       }
402
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');
406       }
407
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');
411       }
412
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'));
417         if (!$path) {
418           $form_state->setErrorByName('logo_path', $this->t('The custom logo path is invalid.'));
419         }
420       }
421       if ($form_state->getValue('favicon_path')) {
422         $path = $this->validatePath($form_state->getValue('favicon_path'));
423         if (!$path) {
424           $form_state->setErrorByName('favicon_path', $this->t('The custom favicon path is invalid.'));
425         }
426       }
427     }
428   }
429
430   /**
431    * {@inheritdoc}
432    */
433   public function submitForm(array &$form, FormStateInterface $form_state) {
434     parent::submitForm($form, $form_state);
435
436     $config_key = $form_state->getValue('config_key');
437     $this->editableConfig = [$config_key];
438     $config = $this->config($config_key);
439
440     // Exclude unnecessary elements before saving.
441     $form_state->cleanValues();
442     $form_state->unsetValue('var');
443     $form_state->unsetValue('config_key');
444
445     $values = $form_state->getValues();
446
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;
453     }
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;
459     }
460     unset($values['logo_upload']);
461     unset($values['favicon_upload']);
462
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']);
467     }
468     if (!empty($values['favicon_path'])) {
469       $values['favicon_path'] = $this->validatePath($values['favicon_path']);
470     }
471
472     if (empty($values['default_favicon']) && !empty($values['favicon_path'])) {
473       $values['favicon_mimetype'] = $this->mimeTypeGuesser->guess($values['favicon_path']);
474     }
475
476     theme_settings_convert_to_config($values, $config)->save();
477   }
478
479   /**
480    * Helper function for the system_theme_settings form.
481    *
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.
485    *
486    * @param string $path
487    *   A path relative to the Drupal root or to the public files directory, or
488    *   a stream wrapper URI.
489    * @return mixed
490    *   A valid path that can be displayed through the theme system, or FALSE if
491    *   the path could not be validated.
492    */
493   protected function validatePath($path) {
494     // Absolute local file paths are invalid.
495     if (drupal_realpath($path) == $path) {
496       return FALSE;
497     }
498     // A path relative to the Drupal root or a fully qualified URI is valid.
499     if (is_file($path)) {
500       return $path;
501     }
502     // Prepend 'public://' for relative file paths within public filesystem.
503     if (file_uri_scheme($path) === FALSE) {
504       $path = 'public://' . $path;
505     }
506     if (is_file($path)) {
507       return $path;
508     }
509     return FALSE;
510   }
511
512 }