ThemeSettingsForm.php 17.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
<?php

/**
 * @file
 * Contains \Drupal\system\Form\ThemeSettingsForm.
 */

namespace Drupal\system\Form;

10
use Drupal\Core\Extension\ThemeHandlerInterface;
11
use Drupal\Core\Form\FormStateInterface;
12
use Drupal\Core\Render\Element;
13
use Drupal\Core\StreamWrapper\PublicStream;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
16
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
17
use Drupal\Core\Config\ConfigFactoryInterface;
18
use Drupal\Core\Extension\ModuleHandlerInterface;
19
use Drupal\Core\Form\ConfigFormBase;
20
21
22
23

/**
 * Displays theme configuration for entire site and individual themes.
 */
24
class ThemeSettingsForm extends ConfigFormBase {
25
26
27
28
29
30
31
32

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

33
34
35
36
37
38
39
  /**
   * The theme handler.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;

40
41
42
43
44
45
46
  /**
   * The MIME type guesser.
   *
   * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
   */
  protected $mimeTypeGuesser;

47
48
49
50
51
52
53
  /**
   * An array of configuration names that should be editable.
   *
   * @var array
   */
  protected $editableConfig = [];

54
55
56
  /**
   * Constructs a ThemeSettingsForm object.
   *
57
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
58
   *   The factory for configuration objects.
59
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
60
   *   The module handler instance to use.
61
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
62
63
64
   *   The theme handler.
   * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
   *   The MIME type guesser instance to use.
65
   */
66
  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, MimeTypeGuesserInterface $mime_type_guesser) {
67
    parent::__construct($config_factory);
68
69

    $this->moduleHandler = $module_handler;
70
    $this->themeHandler = $theme_handler;
71
    $this->mimeTypeGuesser = $mime_type_guesser;
72
73
74
75
76
77
78
79
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
80
      $container->get('module_handler'),
81
82
      $container->get('theme_handler'),
      $container->get('file.mime_type.guesser')
83
84
85
86
87
88
    );
  }

  /**
   * {@inheritdoc}
   */
89
  public function getFormId() {
90
91
92
    return 'system_theme_settings';
  }

93
94
95
96
97
98
99
  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return $this->editableConfig;
  }

100
101
102
  /**
   * {@inheritdoc}
   *
103
   * @param string $theme
104
105
   *   The theme name.
   */
106
  public function buildForm(array $form, FormStateInterface $form_state, $theme = '') {
107
108
    $form = parent::buildForm($form, $form_state);

109
    $themes = $this->themeHandler->listInfo();
110

111
    // Deny access if the theme is not installed or not found.
112
    if (!empty($theme) && (empty($themes[$theme]) || !$themes[$theme]->status)) {
113
114
115
116
      throw new NotFoundHttpException();
    }

    // Default settings are defined in theme_get_setting() in includes/theme.inc
117
118
119
    if ($theme) {
      $var = 'theme_' . $theme . '_settings';
      $config_key = $theme . '.settings';
120
      $themes = $this->themeHandler->listInfo();
121
      $features = $themes[$theme]->info['features'];
122
123
124
125
126
    }
    else {
      $var = 'theme_settings';
      $config_key = 'system.theme.global';
    }
127
128
129
130
    // @todo this is pretty meaningless since we're using theme_get_settings
    //   which means overrides can bleed into active config here. Will be fixed
    //   by https://www.drupal.org/node/2402467.
    $this->editableConfig = [$config_key];
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

    $form['var'] = array(
      '#type' => 'hidden',
      '#value' => $var
    );
    $form['config_key'] = array(
      '#type' => 'hidden',
      '#value' => $config_key
    );

    // Toggle settings
    $toggles = array(
      'node_user_picture' => t('User pictures in posts'),
      'comment_user_picture' => t('User pictures in comments'),
      'comment_user_verification' => t('User verification status in comments'),
      'favicon' => t('Shortcut icon'),
    );

    // Some features are not always available
    $disabled = array();
    if (!user_picture_enabled()) {
      $disabled['toggle_node_user_picture'] = TRUE;
      $disabled['toggle_comment_user_picture'] = TRUE;
    }
    if (!$this->moduleHandler->moduleExists('comment')) {
      $disabled['toggle_comment_user_picture'] = TRUE;
      $disabled['toggle_comment_user_verification'] = TRUE;
    }

    $form['theme_settings'] = array(
      '#type' => 'details',
      '#title' => t('Toggle display'),
163
      '#open' => TRUE,
164
165
166
      '#description' => t('Enable or disable the display of certain page elements.'),
    );
    foreach ($toggles as $name => $title) {
167
168
      if ((!$theme) || in_array($name, $features)) {
        $form['theme_settings']['toggle_' . $name] = array('#type' => 'checkbox', '#title' => $title, '#default_value' => theme_get_setting('features.' . $name, $theme));
169
170
171
172
173
174
175
        // Disable checkboxes for features not supported in the current configuration.
        if (isset($disabled['toggle_' . $name])) {
          $form['theme_settings']['toggle_' . $name]['#disabled'] = TRUE;
        }
      }
    }

176
    if (!Element::children($form['theme_settings'])) {
177
178
179
180
181
182
      // If there is no element in the theme settings details then do not show
      // it -- but keep it in the form if another module wants to alter.
      $form['theme_settings']['#access'] = FALSE;
    }

    // Logo settings, only available when file.module is enabled.
183
    if ((!$theme) || in_array('logo', $features) && $this->moduleHandler->moduleExists('file')) {
184
185
186
      $form['logo'] = array(
        '#type' => 'details',
        '#title' => t('Logo image settings'),
187
        '#open' => TRUE,
188
189
190
191
192
193
194
195
196
197
        '#states' => array(
          // Hide the logo image settings fieldset when logo display is disabled.
          'invisible' => array(
            'input[name="toggle_logo"]' => array('checked' => FALSE),
          ),
        ),
      );
      $form['logo']['default_logo'] = array(
        '#type' => 'checkbox',
        '#title' => t('Use the default logo supplied by the theme'),
198
        '#default_value' => theme_get_setting('logo.use_default', $theme),
199
200
201
202
203
204
205
206
207
208
209
210
211
212
        '#tree' => FALSE,
      );
      $form['logo']['settings'] = array(
        '#type' => 'container',
        '#states' => array(
          // Hide the logo settings when using the default logo.
          'invisible' => array(
            'input[name="default_logo"]' => array('checked' => TRUE),
          ),
        ),
      );
      $form['logo']['settings']['logo_path'] = array(
        '#type' => 'textfield',
        '#title' => t('Path to custom logo'),
213
        '#default_value' => theme_get_setting('logo.path', $theme),
214
215
216
217
218
219
220
221
222
      );
      $form['logo']['settings']['logo_upload'] = array(
        '#type' => 'file',
        '#title' => t('Upload logo image'),
        '#maxlength' => 40,
        '#description' => t("If you don't have direct file access to the server, use this field to upload your logo.")
      );
    }

223
    if (((!$theme) || in_array('favicon', $features)) && $this->moduleHandler->moduleExists('file')) {
224
225
226
      $form['favicon'] = array(
        '#type' => 'details',
        '#title' => t('Shortcut icon settings'),
227
        '#open' => TRUE,
228
229
230
231
232
233
234
235
236
237
238
239
        '#description' => t("Your shortcut icon, or 'favicon', is displayed in the address bar and bookmarks of most browsers."),
        '#states' => array(
          // Hide the shortcut icon settings fieldset when shortcut icon display
          // is disabled.
          'invisible' => array(
            'input[name="toggle_favicon"]' => array('checked' => FALSE),
          ),
        ),
      );
      $form['favicon']['default_favicon'] = array(
        '#type' => 'checkbox',
        '#title' => t('Use the default shortcut icon supplied by the theme'),
240
        '#default_value' => theme_get_setting('favicon.use_default', $theme),
241
242
243
244
245
246
247
248
249
250
251
252
253
      );
      $form['favicon']['settings'] = array(
        '#type' => 'container',
        '#states' => array(
          // Hide the favicon settings when using the default favicon.
          'invisible' => array(
            'input[name="default_favicon"]' => array('checked' => TRUE),
          ),
        ),
      );
      $form['favicon']['settings']['favicon_path'] = array(
        '#type' => 'textfield',
        '#title' => t('Path to custom icon'),
254
        '#default_value' => theme_get_setting('favicon.path', $theme),
255
256
257
258
259
260
261
262
263
264
      );
      $form['favicon']['settings']['favicon_upload'] = array(
        '#type' => 'file',
        '#title' => t('Upload icon image'),
        '#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon.")
      );
    }

    // Inject human-friendly values and form element descriptions for logo and
    // favicon.
265
    foreach (array('logo' => 'logo.svg', 'favicon' => 'favicon.ico') as $type => $default) {
266
267
268
269
270
271
272
273
274
275
276
277
278
279
      if (isset($form[$type]['settings'][$type . '_path'])) {
        $element = &$form[$type]['settings'][$type . '_path'];

        // If path is a public:// URI, display the path relative to the files
        // directory; stream wrappers are not end-user friendly.
        $original_path = $element['#default_value'];
        $friendly_path = NULL;
        if (file_uri_scheme($original_path) == 'public') {
          $friendly_path = file_uri_target($original_path);
          $element['#default_value'] = $friendly_path;
        }

        // Prepare local file path for description.
        if ($original_path && isset($friendly_path)) {
280
          $local_file = strtr($original_path, array('public:/' => PublicStream::basePath()));
281
        }
282
283
        elseif ($theme) {
          $local_file = drupal_get_path('theme', $theme) . '/' . $default;
284
285
        }
        else {
286
          $local_file = \Drupal::theme()->getActiveTheme()->getPath() . '/' . $default;
287
288
289
290
291
292
293
294
295
296
        }

        $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>.', array(
          '@implicit-public-file' => isset($friendly_path) ? $friendly_path : $default,
          '@explicit-file' => file_uri_scheme($original_path) !== FALSE ? $original_path : 'public://' . $default,
          '@local-file' => $local_file,
        ));
      }
    }

297
    if ($theme) {
298
      // Call engine-specific settings.
299
      $function = $themes[$theme]->prefix . '_engine_settings';
300
301
302
303
      if (function_exists($function)) {
        $form['engine_specific'] = array(
          '#type' => 'details',
          '#title' => t('Theme-engine-specific settings'),
304
          '#open' => TRUE,
305
          '#description' => t('These settings only exist for the themes based on the %engine theme engine.', array('%engine' => $themes[$theme]->prefix)),
306
307
308
309
310
        );
        $function($form, $form_state);
      }

      // Create a list which includes the current theme and all its base themes.
311
312
313
      if (isset($themes[$theme]->base_themes)) {
        $theme_keys = array_keys($themes[$theme]->base_themes);
        $theme_keys[] = $theme;
314
315
      }
      else {
316
        $theme_keys = array($theme);
317
318
319
320
321
      }

      // Save the name of the current theme (if any), so that we can temporarily
      // override the current theme and allow theme_get_setting() to work
      // without having to pass the theme name to it.
322
323
324
325
326
      $default_active_theme = \Drupal::theme()->getActiveTheme();
      $default_theme = $default_active_theme->getName();
      /** @var \Drupal\Core\Theme\ThemeInitialization $theme_initialization */
      $theme_initialization = \Drupal::service('theme.initialization');
      \Drupal::theme()->setActiveTheme($theme_initialization->getActiveThemeByName($theme));
327
328
329
330

      // Process the theme and all its base themes.
      foreach ($theme_keys as $theme) {
        // Include the theme-settings.php file.
331
        $filename = DRUPAL_ROOT . '/' . $themes[$theme]->getPath() . '/theme-settings.php';
332
333
334
335
336
337
338
339
340
341
342
343
344
        if (file_exists($filename)) {
          require_once $filename;
        }

        // Call theme-specific settings.
        $function = $theme . '_form_system_theme_settings_alter';
        if (function_exists($function)) {
          $function($form, $form_state);
        }
      }

      // Restore the original current theme.
      if (isset($default_theme)) {
345
        \Drupal::theme()->setActiveTheme($default_active_theme);
346
347
      }
      else {
348
        \Drupal::theme()->resetActiveTheme();
349
350
351
352
353
354
355
356
357
      }
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
358
  public function validateForm(array &$form, FormStateInterface $form_state) {
359
360
361
362
363
364
365
    parent::validateForm($form, $form_state);

    if ($this->moduleHandler->moduleExists('file')) {
      // Handle file uploads.
      $validators = array('file_validate_is_image' => array());

      // Check for a new uploaded logo.
366
      $file = file_save_upload('logo_upload', $validators, FALSE, 0);
367
368
369
370
      if (isset($file)) {
        // File upload was attempted.
        if ($file) {
          // Put the temporary file in form_values so we can save it on submit.
371
          $form_state->setValue('logo_upload', $file);
372
373
374
        }
        else {
          // File upload failed.
375
          $form_state->setErrorByName('logo_upload', $this->t('The logo could not be uploaded.'));
376
377
378
379
380
381
        }
      }

      $validators = array('file_validate_extensions' => array('ico png gif jpg jpeg apng svg'));

      // Check for a new uploaded favicon.
382
      $file = file_save_upload('favicon_upload', $validators, FALSE, 0);
383
384
385
386
      if (isset($file)) {
        // File upload was attempted.
        if ($file) {
          // Put the temporary file in form_values so we can save it on submit.
387
          $form_state->setValue('favicon_upload', $file);
388
389
390
        }
        else {
          // File upload failed.
391
          $form_state->setErrorByName('favicon_upload', $this->t('The favicon could not be uploaded.'));
392
393
394
395
396
        }
      }

      // If the user provided a path for a logo or favicon file, make sure a file
      // exists at that path.
397
398
      if ($form_state->getValue('logo_path')) {
        $path = $this->validatePath($form_state->getValue('logo_path'));
399
        if (!$path) {
400
          $form_state->setErrorByName('logo_path', $this->t('The custom logo path is invalid.'));
401
402
        }
      }
403
404
      if ($form_state->getValue('favicon_path')) {
        $path = $this->validatePath($form_state->getValue('favicon_path'));
405
        if (!$path) {
406
          $form_state->setErrorByName('favicon_path', $this->t('The custom favicon path is invalid.'));
407
408
409
410
411
412
413
414
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
415
  public function submitForm(array &$form, FormStateInterface $form_state) {
416
417
    parent::submitForm($form, $form_state);

418
419
420
    $config_key = $form_state->getValue('config_key');
    $this->editableConfig = [$config_key];
    $config = $this->config($config_key);
421
422

    // Exclude unnecessary elements before saving.
423
    $form_state->cleanValues();
424
425
    $form_state->unsetValue('var');
    $form_state->unsetValue('config_key');
426

427
    $values = $form_state->getValues();
428
429
430

    // If the user uploaded a new logo or favicon, save it to a permanent location
    // and use it in place of the default theme-provided file.
431
432
433
434
435
436
437
438
439
440
441
442
443
444
    if (!empty($values['logo_upload'])) {
      $filename = file_unmanaged_copy($values['logo_upload']->getFileUri());
      $values['default_logo'] = 0;
      $values['logo_path'] = $filename;
      $values['toggle_logo'] = 1;
    }
    if (!empty($values['favicon_upload'])) {
      $filename = file_unmanaged_copy($values['favicon_upload']->getFileUri());
      $values['default_favicon'] = 0;
      $values['favicon_path'] = $filename;
      $values['toggle_favicon'] = 1;
    }
    unset($values['logo_upload']);
    unset($values['favicon_upload']);
445

446
447
448
449
450
451
452
453
    // If the user entered a path relative to the system files directory for
    // a logo or favicon, store a public:// URI so the theme system can handle it.
    if (!empty($values['logo_path'])) {
      $values['logo_path'] = $this->validatePath($values['logo_path']);
    }
    if (!empty($values['favicon_path'])) {
      $values['favicon_path'] = $this->validatePath($values['favicon_path']);
    }
454

455
456
    if (empty($values['default_favicon']) && !empty($values['favicon_path'])) {
      $values['favicon_mimetype'] = $this->mimeTypeGuesser->guess($values['favicon_path']);
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
    }

    theme_settings_convert_to_config($values, $config)->save();
  }

  /**
   * Helper function for the system_theme_settings form.
   *
   * Attempts to validate normal system paths, paths relative to the public files
   * directory, or stream wrapper URIs. If the given path is any of the above,
   * returns a valid path or URI that the theme system can display.
   *
   * @param string $path
   *   A path relative to the Drupal root or to the public files directory, or
   *   a stream wrapper URI.
   * @return mixed
   *   A valid path that can be displayed through the theme system, or FALSE if
   *   the path could not be validated.
   */
  protected function validatePath($path) {
    // Absolute local file paths are invalid.
    if (drupal_realpath($path) == $path) {
      return FALSE;
    }
    // A path relative to the Drupal root or a fully qualified URI is valid.
    if (is_file($path)) {
      return $path;
    }
    // Prepend 'public://' for relative file paths within public filesystem.
    if (file_uri_scheme($path) === FALSE) {
      $path = 'public://' . $path;
    }
    if (is_file($path)) {
      return $path;
    }
    return FALSE;
  }

}