system.admin.inc 108 KB
Newer Older
1 2 3
<?php

/**
4 5 6 7
 * @file
 * Admin page callbacks for the system module.
 */

8 9
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
10
use Symfony\Component\HttpFoundation\JsonResponse;
11
use Symfony\Component\HttpFoundation\Response;
12
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
13
use Drupal\Core\Datetime\DrupalDateTime;
14

15 16 17 18 19 20 21 22 23
/**
 * Menu callback; Provide the administration overview page.
 */
function system_admin_config_page() {
  // Check for status report errors.
  if (system_status(TRUE) && user_access('administer site configuration')) {
    drupal_set_message(t('One or more problems were detected with your Drupal installation. Check the <a href="@status">status report</a> for more information.', array('@status' => url('admin/reports/status'))), 'error');
  }
  $blocks = array();
24 25
  if ($system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/config', 'module' => 'system'))) {
    $system_link = reset($system_link);
26
    $query = Drupal::entityQuery('menu_link')
27 28 29 30 31 32 33
      ->condition('link_path', 'admin/help', '<>')
      ->condition('menu_name', $system_link->menu_name)
      ->condition('plid', $system_link->id())
      ->condition('hidden', 0);
    $result = $query->execute();
    if (!empty($result)) {
      $menu_links = menu_link_load_multiple($result);
34

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
      foreach ($menu_links as $item) {
        _menu_link_translate($item);
        if (!$item['access']) {
          continue;
        }
        // The link description, either derived from 'description' in hook_menu()
        // or customized via menu module is used as title attribute.
        if (!empty($item['localized_options']['attributes']['title'])) {
          $item['description'] = $item['localized_options']['attributes']['title'];
          unset($item['localized_options']['attributes']['title']);
        }
        $block = $item;
        $block['content'] = '';
        $block['content'] .= theme('admin_block_content', array('content' => system_admin_menu_block($item)));
        if (!empty($block['content'])) {
          $block['show'] = TRUE;
        }

        // Prepare for sorting as in function _menu_tree_check_access().
        // The weight is offset so it is always positive, with a uniform 5-digits.
        $blocks[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $block;
      }
57 58 59 60
    }
  }
  if ($blocks) {
    ksort($blocks);
61
    return theme('admin_page', array('blocks' => $blocks));
62 63 64 65 66
  }
  else {
    return t('You do not have any administrative items.');
  }
}
67 68 69

/**
 * Provide a single block from the administration menu as a page.
70
 *
71
 * This function is often a destination for these blocks.
72
 * For example, 'admin/structure/types' needs to have a destination to be valid
73 74
 * in the Drupal menu system, but too much information there might be
 * hidden, so we supply the contents of the block.
75 76 77
 *
 * @return
 *   The output HTML.
78 79 80
 */
function system_admin_menu_block_page() {
  $item = menu_get_item();
81
  if ($content = system_admin_menu_block($item)) {
82
    $output = theme('admin_block_content', array('content' => $content));
83 84 85 86
  }
  else {
    $output = t('You do not have any administrative items.');
  }
87 88
  return $output;
}
89 90

/**
91
 * Menu callback; prints a listing of admin tasks, organized by module.
92
 */
93
function system_admin_index() {
94
  $module_info = system_get_info('module');
95
  foreach ($module_info as $module => $info) {
96
    $module_info[$module] = new stdClass();
97 98 99
    $module_info[$module]->info = $info;
  }
  uasort($module_info, 'system_sort_modules_by_info_name');
100 101
  $menu_items = array();

102
  foreach ($module_info as $module => $info) {
103
    // Only display a section if there are any available tasks.
104 105
    if ($admin_tasks = system_get_module_admin_tasks($module, $info->info)) {
      // Sort links by title.
106
      uasort($admin_tasks, 'drupal_sort_title');
107 108 109 110 111 112
      // Move 'Configure permissions' links to the bottom of each section.
      $permission_key = "admin/people/permissions#module-$module";
      if (isset($admin_tasks[$permission_key])) {
        $permission_task = $admin_tasks[$permission_key];
        unset($admin_tasks[$permission_key]);
        $admin_tasks[$permission_key] = $permission_task;
113 114
      }

115
      $menu_items[$info->info['name']] = array($info->info['description'], $admin_tasks);
116 117
    }
  }
118
  return theme('system_admin_index', array('menu_items' => $menu_items));
119 120
}

121 122 123 124 125
/**
 * Menu callback; displays a listing of all themes.
 */
function system_themes_page() {
  // Get current list of themes.
126
  $themes = system_rebuild_theme_data();
127
  uasort($themes, 'system_sort_modules_by_info_name');
128

129
  $theme_default = config('system.theme')->get('default');
130
  $theme_groups  = array();
131
  $admin_theme = config('system.theme')->get('admin');
132 133

  foreach ($themes as &$theme) {
134 135 136
    if (!empty($theme->info['hidden'])) {
      continue;
    }
137
    $theme->is_default = ($theme->name == $theme_default);
138

139 140
    // Identify theme screenshot.
    $theme->screenshot = NULL;
141 142
    // Create a list which includes the current theme and all its base themes.
    if (isset($themes[$theme->name]->base_themes)) {
143 144
      $theme_keys = array_keys($themes[$theme->name]->base_themes);
      $theme_keys[] = $theme->name;
145 146 147 148 149 150 151
    }
    else {
      $theme_keys = array($theme->name);
    }
    // Look for a screenshot in the current theme or in its closest ancestor.
    foreach (array_reverse($theme_keys) as $theme_key) {
      if (isset($themes[$theme_key]) && file_exists($themes[$theme_key]->info['screenshot'])) {
152
        $theme->screenshot = array(
153
          'uri' => $themes[$theme_key]->info['screenshot'],
154 155 156 157
          'alt' => t('Screenshot for !theme theme', array('!theme' => $theme->info['name'])),
          'title' => t('Screenshot for !theme theme', array('!theme' => $theme->info['name'])),
          'attributes' => array('class' => array('screenshot')),
        );
158 159 160
        break;
      }
    }
161

162 163 164 165 166 167
    if (empty($theme->status)) {
     // Ensure this theme is compatible with this version of core.
     // Require the 'content' region to make sure the main page
     // content has a common place in all themes.
      $theme->incompatible_core = !isset($theme->info['core']) || ($theme->info['core'] != DRUPAL_CORE_COMPATIBILITY) || (!isset($theme->info['regions']['content']));
      $theme->incompatible_php = version_compare(phpversion(), $theme->info['php']) < 0;
168 169 170 171
      // Confirmed that the base theme is available.
      $theme->incompatible_base = (isset($theme->info['base theme']) && !isset($themes[$theme->info['base theme']]));
      // Confirm that the theme engine is available.
      $theme->incompatible_engine = (isset($theme->info['engine']) && !isset($theme->owner));
172
    }
173 174
    $query['token'] = drupal_get_token('system-theme-operation-link');
    $theme->operations = array();
175
    if (!empty($theme->status) || !$theme->incompatible_core && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine) {
176 177 178 179 180 181 182 183
      // Create the operations links.
      $query['theme'] = $theme->name;
      if (drupal_theme_access($theme)) {
        $theme->operations[] = array(
          'title' => t('Settings'),
          'href' => 'admin/appearance/settings/' . $theme->name,
          'attributes' => array('title' => t('Settings for !theme theme', array('!theme' => $theme->info['name']))),
        );
184
      }
185 186
      if (!empty($theme->status)) {
        if (!$theme->is_default) {
187 188 189 190 191 192 193 194
          if ($theme->name != $admin_theme) {
            $theme->operations[] = array(
              'title' => t('Disable'),
              'href' => 'admin/appearance/disable',
              'query' => $query,
              'attributes' => array('title' => t('Disable !theme theme', array('!theme' => $theme->info['name']))),
            );
          }
195 196 197 198 199 200 201
          $theme->operations[] = array(
            'title' => t('Set default'),
            'href' => 'admin/appearance/default',
            'query' => $query,
            'attributes' => array('title' => t('Set !theme as default theme', array('!theme' => $theme->info['name']))),
          );
        }
202
        $admin_theme_options[$theme->name] = $theme->info['name'];
203 204 205 206 207 208 209 210
      }
      else {
        $theme->operations[] = array(
          'title' => t('Enable'),
          'href' => 'admin/appearance/enable',
          'query' => $query,
          'attributes' => array('title' => t('Enable !theme theme', array('!theme' => $theme->info['name']))),
        );
211 212 213 214 215 216
        $theme->operations[] = array(
          'title' => t('Enable and set default'),
          'href' => 'admin/appearance/default',
          'query' => $query,
          'attributes' => array('title' => t('Enable !theme as default theme', array('!theme' => $theme->info['name']))),
        );
217 218
      }
    }
219 220 221 222 223 224 225 226

    // Add notes to default and administration theme.
    $theme->notes = array();
    $theme->classes = array();
    if ($theme->is_default) {
      $theme->classes[] = 'theme-default';
      $theme->notes[] = t('default theme');
    }
227 228 229 230
    if ($theme->name == $admin_theme || ($theme->is_default && $admin_theme == '0')) {
      $theme->classes[] = 'theme-admin';
      $theme->notes[] = t('admin theme');
    }
231 232 233

    // Sort enabled and disabled themes into their own groups.
    $theme_groups[$theme->status ? 'enabled' : 'disabled'][] = $theme;
234 235
  }

236 237 238
  // There are two possible theme groups.
  $theme_group_titles = array(
    'enabled' => format_plural(count($theme_groups['enabled']), 'Enabled theme', 'Enabled themes'),
239
  );
240 241
  if (!empty($theme_groups['disabled'])) {
    $theme_group_titles['disabled'] = format_plural(count($theme_groups['disabled']), 'Disabled theme', 'Disabled themes');
242
  }
243

244 245 246 247 248 249 250 251 252 253 254 255 256 257
  uasort($theme_groups['enabled'], 'system_sort_themes');
  drupal_alter('system_themes_page', $theme_groups);

  $admin_form = drupal_get_form('system_themes_admin_form', $admin_theme_options);
  return theme('system_themes_page', array('theme_groups' => $theme_groups, 'theme_group_titles' => $theme_group_titles)) . drupal_render($admin_form);
}

/**
 * Form to select the administration theme.
 *
 * @ingroup forms
 * @see system_themes_admin_form_submit()
 */
function system_themes_admin_form($form, &$form_state, $theme_options) {
258 259
  // Administration theme settings.
  $form['admin_theme'] = array(
260
    '#type' => 'details',
261 262 263 264
    '#title' => t('Administration theme'),
  );
  $form['admin_theme']['admin_theme'] = array(
    '#type' => 'select',
265
    '#options' => array(0 => t('Default theme')) + $theme_options,
266
    '#title' => t('Administration theme'),
267
    '#description' => t('Choose "Default theme" to always use the same theme as the rest of the site.'),
268
    '#default_value' => config('system.theme')->get('admin'),
269
  );
270
  $form['admin_theme']['actions'] = array('#type' => 'actions');
271
  $form['admin_theme']['actions']['submit'] = array(
272 273 274 275 276 277
    '#type' => 'submit',
    '#value' => t('Save configuration'),
  );
  return $form;
}

278
/**
279
 * Process system_themes_admin_form form submissions.
280
 */
281 282
function system_themes_admin_form_submit($form, &$form_state) {
  drupal_set_message(t('The configuration options have been saved.'));
283
  config('system.theme')->set('admin', $form_state['values']['admin_theme'])->save();
284
}
285

286 287 288 289 290 291 292
/**
 * Menu callback; Enables a theme.
 */
function system_theme_enable() {
  if (isset($_REQUEST['theme']) && isset($_REQUEST['token']) && drupal_valid_token($_REQUEST['token'], 'system-theme-operation-link')) {
    $theme = $_REQUEST['theme'];
    // Get current list of themes.
293
    $themes = list_themes();
294 295 296 297 298 299 300 301

    // Check if the specified theme is one recognized by the system.
    if (!empty($themes[$theme])) {
      theme_enable(array($theme));
      drupal_set_message(t('The %theme theme has been enabled.', array('%theme' => $themes[$theme]->info['name'])));
    }
    else {
      drupal_set_message(t('The %theme theme was not found.', array('%theme' => $theme)), 'error');
302
    }
303
    drupal_goto('admin/appearance');
304
  }
305
  throw new AccessDeniedHttpException();
306
}
307

308 309 310 311 312 313 314
/**
 * Menu callback; Disables a theme.
 */
function system_theme_disable() {
  if (isset($_REQUEST['theme']) && isset($_REQUEST['token']) && drupal_valid_token($_REQUEST['token'], 'system-theme-operation-link')) {
    $theme = $_REQUEST['theme'];
    // Get current list of themes.
315
    $themes = list_themes();
316 317 318

    // Check if the specified theme is one recognized by the system.
    if (!empty($themes[$theme])) {
319
      // Do not disable the default or admin theme.
320
      if ($theme === config('system.theme')->get('default') || $theme === config('system.theme')->get('admin')) {
321 322 323 324 325
        drupal_set_message(t('%theme is the default theme and cannot be disabled.', array('%theme' => $themes[$theme]->info['name'])), 'error');
      }
      else {
        theme_disable(array($theme));
        drupal_set_message(t('The %theme theme has been disabled.', array('%theme' => $themes[$theme]->info['name'])));
326 327
      }
    }
328 329
    else {
      drupal_set_message(t('The %theme theme was not found.', array('%theme' => $theme)), 'error');
330
    }
331
    drupal_goto('admin/appearance');
332
  }
333
  throw new AccessDeniedHttpException();
334
}
335

336 337 338 339 340 341 342
/**
 * Menu callback; Set the default theme.
 */
function system_theme_default() {
  if (isset($_REQUEST['theme']) && isset($_REQUEST['token']) && drupal_valid_token($_REQUEST['token'], 'system-theme-operation-link')) {
    $theme = $_REQUEST['theme'];
    // Get current list of themes.
343
    $themes = list_themes();
344 345 346 347 348 349 350 351

    // Check if the specified theme is one recognized by the system.
    if (!empty($themes[$theme])) {
      // Enable the theme if it is currently disabled.
      if (empty($themes[$theme]->status)) {
       theme_enable(array($theme));
      }
      // Set the default theme.
352 353 354
      config('system.theme')
        ->set('default', $theme)
        ->save();
355

356 357 358 359 360 361 362
      // Rebuild the menu. This duplicates the menu_router_rebuild() in
      // theme_enable(). However, modules must know the current default theme in
      // order to use this information in hook_menu() or hook_menu_alter()
      // implementations, and doing the variable_set() before the theme_enable()
      // could result in a race condition where the theme is default but not
      // enabled.
      menu_router_rebuild();
363

364 365
      // The status message depends on whether an admin theme is currently in use:
      // a value of 0 means the admin theme is set to be the default theme.
366
      $admin_theme = config('system.theme')->get('admin');
367
      if ($admin_theme != 0 && $admin_theme != $theme) {
368 369 370 371 372 373 374 375 376 377 378 379 380 381
        drupal_set_message(t('Please note that the administration theme is still set to the %admin_theme theme; consequently, the theme on this page remains unchanged. All non-administrative sections of the site, however, will show the selected %selected_theme theme by default.', array(
          '%admin_theme' => $themes[$admin_theme]->info['name'],
          '%selected_theme' => $themes[$theme]->info['name'],
        )));
      }
      else {
        drupal_set_message(t('%theme is now the default theme.', array('%theme' => $themes[$theme]->info['name'])));
      }
    }
    else {
      drupal_set_message(t('The %theme theme was not found.', array('%theme' => $theme)), 'error');
    }
    drupal_goto('admin/appearance');
  }
382
  throw new AccessDeniedHttpException();
383 384 385 386
}

/**
 * Form builder; display theme configuration for entire site and individual themes.
387
 *
388 389 390 391
 * @param $key
 *   A theme name.
 * @return
 *   The form structure.
392
 * @ingroup forms
393
 * @see system_theme_settings_submit()
394
 */
395
function system_theme_settings($form, &$form_state, $key = '') {
396
  // Default settings are defined in theme_get_setting() in includes/theme.inc
397
  if ($key) {
398
    $var = 'theme_' . $key . '_settings';
399
    $themes = list_themes();
400 401 402 403 404 405 406 407 408 409
    $features = $themes[$key]->info['features'];
  }
  else {
    $var = 'theme_settings';
  }

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

  // Toggle settings
  $toggles = array(
410 411 412 413 414 415 416 417 418
    'logo'                      => t('Logo'),
    'name'                      => t('Site name'),
    'slogan'                    => t('Site slogan'),
    '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'),
    'main_menu'                 => t('Main menu'),
    'secondary_menu'            => t('Secondary menu'),
419 420 421 422
  );

  // Some features are not always available
  $disabled = array();
423
  if (!user_picture_enabled()) {
424 425 426
    $disabled['toggle_node_user_picture'] = TRUE;
    $disabled['toggle_comment_user_picture'] = TRUE;
  }
427 428 429 430
  if (!module_exists('comment')) {
    $disabled['toggle_comment_user_picture'] = TRUE;
    $disabled['toggle_comment_user_verification'] = TRUE;
  }
431 432

  $form['theme_settings'] = array(
433
    '#type' => 'details',
434 435 436 437 438
    '#title' => t('Toggle display'),
    '#description' => t('Enable or disable the display of certain page elements.'),
  );
  foreach ($toggles as $name => $title) {
    if ((!$key) || in_array($name, $features)) {
439
      $form['theme_settings']['toggle_' . $name] = array('#type' => 'checkbox', '#title' => $title, '#default_value' => theme_get_setting('toggle_' . $name, $key));
440
      // Disable checkboxes for features not supported in the current configuration.
441 442
      if (isset($disabled['toggle_' . $name])) {
        $form['theme_settings']['toggle_' . $name]['#disabled'] = TRUE;
443 444 445 446
      }
    }
  }

447
  if (!element_children($form['theme_settings'])) {
448
    // If there is no element in the theme settings details then do not show
449 450 451
    // it -- but keep it in the form if another module wants to alter.
    $form['theme_settings']['#access'] = FALSE;
  }
452

453 454
  // Logo settings, only available when file.module is enabled.
  if ((!$key) || in_array('logo', $features) && module_exists('file')) {
455
    $form['logo'] = array(
456
      '#type' => 'details',
457
      '#title' => t('Logo image settings'),
458
      '#attributes' => array('class' => array('theme-settings-bottom')),
459
    );
460
    $form['logo']['default_logo'] = array(
461
      '#type' => 'checkbox',
462
      '#title' => t('Use the default logo supplied by the theme'),
463
      '#default_value' => theme_get_setting('default_logo', $key),
464 465
      '#tree' => FALSE,
    );
466 467 468 469 470 471 472 473 474 475
    $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(
476 477
      '#type' => 'textfield',
      '#title' => t('Path to custom logo'),
478
      '#default_value' => theme_get_setting('logo_path', $key),
479 480
    );
    $form['logo']['settings']['logo_upload'] = array(
481 482 483 484 485 486 487
      '#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.")
    );
  }

488
  if ((!$key) || in_array('favicon', $features) && module_exists('file')) {
489
    $form['favicon'] = array(
490
      '#type' => 'details',
491
      '#title' => t('Shortcut icon settings'),
492
      '#description' => t("Your shortcut icon, or 'favicon', is displayed in the address bar and bookmarks of most browsers."),
493 494 495
    );
    $form['favicon']['default_favicon'] = array(
      '#type' => 'checkbox',
496
      '#title' => t('Use the default shortcut icon supplied by the theme'),
497
      '#default_value' => theme_get_setting('default_favicon', $key),
498
    );
499 500 501 502 503 504 505 506 507 508
    $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(
509 510
      '#type' => 'textfield',
      '#title' => t('Path to custom icon'),
511
      '#default_value' => theme_get_setting('favicon_path', $key),
512
    );
513
    $form['favicon']['settings']['favicon_upload'] = array(
514 515 516 517 518 519
      '#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.")
    );
  }

520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
  // Inject human-friendly values and form element descriptions for logo and
  // favicon.
  foreach (array('logo' => 'logo.png', 'favicon' => 'favicon.ico') as $type => $default) {
    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)) {
        $local_file = strtr($original_path, array('public:/' => variable_get('file_public_path', conf_path() . '/files')));
      }
      elseif ($key) {
        $local_file = drupal_get_path('theme', $key) . '/' . $default;
      }
      else {
        $local_file = path_to_theme() . '/' . $default;
      }

      $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,
      ));
    }
  }

554
  if ($key) {
555
    // Call engine-specific settings.
556
    $function = $themes[$key]->prefix . '_engine_settings';
557
    if (function_exists($function)) {
558
      $form['engine_specific'] = array(
559
        '#type' => 'details',
560 561 562 563
        '#title' => t('Theme-engine-specific settings'),
        '#description' => t('These settings only exist for the themes based on the %engine theme engine.', array('%engine' => $themes[$key]->prefix)),
      );
      $function($form, $form_state);
564
    }
565 566 567 568 569

    // Create a list which includes the current theme and all its base themes.
    if (isset($themes[$key]->base_themes)) {
      $theme_keys = array_keys($themes[$key]->base_themes);
      $theme_keys[] = $key;
570
    }
571 572 573 574 575 576 577 578 579 580 581 582 583
    else {
      $theme_keys = array($key);
    }

    // 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.
    $default_theme = !empty($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : NULL;
    $GLOBALS['theme_key'] = $key;

    // Process the theme and all its base themes.
    foreach ($theme_keys as $theme) {
      // Include the theme-settings.php file.
584
      $filename = DRUPAL_ROOT . '/' . str_replace("/$theme.info.yml", '', $themes[$theme]->filename) . '/theme-settings.php';
585 586 587 588 589 590 591 592
      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);
593 594
      }
    }
595 596

    // Restore the original current theme.
597
    if (isset($default_theme)) {
598 599 600 601 602
      $GLOBALS['theme_key'] = $default_theme;
    }
    else {
      unset($GLOBALS['theme_key']);
    }
603 604
  }

605
  $form = system_settings_form($form);
606
  // We don't want to call system_settings_form_submit(), so change #submit.
607 608
  array_pop($form['#submit']);
  $form['#submit'][] = 'system_theme_settings_submit';
609
  $form['#validate'][] = 'system_theme_settings_validate';
610 611 612
  return $form;
}

613 614 615 616
/**
 * Validator for the system_theme_settings() form.
 */
function system_theme_settings_validate($form, &$form_state) {
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
  if (module_exists('file')) {
    // Handle file uploads.
    $validators = array('file_validate_is_image' => array());

    // Check for a new uploaded logo.
    $file = file_save_upload('logo_upload', $validators);
    if (isset($file)) {
      // File upload was attempted.
      if ($file) {
        // Put the temporary file in form_values so we can save it on submit.
        $form_state['values']['logo_upload'] = $file;
      }
      else {
        // File upload failed.
        form_set_error('logo_upload', t('The logo could not be uploaded.'));
      }
633 634
    }

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

637 638 639 640 641 642 643 644 645 646 647 648
    // Check for a new uploaded favicon.
    $file = file_save_upload('favicon_upload', $validators);
    if (isset($file)) {
      // File upload was attempted.
      if ($file) {
        // Put the temporary file in form_values so we can save it on submit.
        $form_state['values']['favicon_upload'] = $file;
      }
      else {
        // File upload failed.
        form_set_error('favicon_upload', t('The favicon could not be uploaded.'));
      }
649 650
    }

651 652 653 654 655 656 657
    // If the user provided a path for a logo or favicon file, make sure a file
    // exists at that path.
    if ($form_state['values']['logo_path']) {
      $path = _system_theme_settings_validate_path($form_state['values']['logo_path']);
      if (!$path) {
        form_set_error('logo_path', t('The custom logo path is invalid.'));
      }
658
    }
659 660 661 662 663
    if ($form_state['values']['favicon_path']) {
      $path = _system_theme_settings_validate_path($form_state['values']['favicon_path']);
      if (!$path) {
        form_set_error('favicon_path', t('The custom favicon path is invalid.'));
      }
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682
    }
  }
}

/**
 * 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 $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.
 */
function _system_theme_settings_validate_path($path) {
683 684 685 686 687 688
  // 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)) {
689 690
    return $path;
  }
691 692 693 694 695 696
  // Prepend 'public://' for relative file paths within public filesystem.
  if (file_uri_scheme($path) === FALSE) {
    $path = 'public://' . $path;
  }
  if (is_file($path)) {
    return $path;
697 698 699 700
  }
  return FALSE;
}

701 702 703
/**
 * Process system_theme_settings form submissions.
 */
704
function system_theme_settings_submit($form, &$form_state) {
705 706
  // Exclude unnecessary elements before saving.
  form_state_values_clean($form_state);
707
  $values = $form_state['values'];
708

709 710 711 712 713
  // Extract the name of the theme from the submitted form values, then
  // remove it from the array so that it is not saved as part of the variable.
  $key = $values['var'];
  unset($values['var']);

714 715
  // 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.
716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
  if (module_exists('file')) {
    if ($file = $values['logo_upload']) {
      unset($values['logo_upload']);
      $filename = file_unmanaged_copy($file->uri);
      $values['default_logo'] = 0;
      $values['logo_path'] = $filename;
      $values['toggle_logo'] = 1;
    }
    if ($file = $values['favicon_upload']) {
      unset($values['favicon_upload']);
      $filename = file_unmanaged_copy($file->uri);
      $values['default_favicon'] = 0;
      $values['favicon_path'] = $filename;
      $values['toggle_favicon'] = 1;
    }
731

732 733 734 735 736 737 738 739
    // 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'] = _system_theme_settings_validate_path($values['logo_path']);
    }
    if (!empty($values['favicon_path'])) {
      $values['favicon_path'] = _system_theme_settings_validate_path($values['favicon_path']);
    }
740

741 742 743
    if (empty($values['default_favicon']) && !empty($values['favicon_path'])) {
      $values['favicon_mimetype'] = file_get_mimetype($values['favicon_path']);
    }
744
  }
745

746 747
  variable_set($key, $values);
  drupal_set_message(t('The configuration options have been saved.'));
748

749
  cache_invalidate_tags(array('content' => TRUE));
750 751
}

752
/**
753 754 755
 * Recursively check compatibility.
 *
 * @param $incompatible
756 757
 *   An associative array which at the end of the check contains all
 *   incompatible files as the keys, their values being TRUE.
758 759 760 761 762
 * @param $files
 *   The set of files that will be tested.
 * @param $file
 *   The file at which the check starts.
 * @return
763 764
 *   Returns TRUE if an incompatible file is found, NULL (no return value)
 *   otherwise.
765 766 767 768 769
 */
function _system_is_incompatible(&$incompatible, $files, $file) {
  if (isset($incompatible[$file->name])) {
    return TRUE;
  }
770 771 772
  // Recursively traverse required modules, looking for incompatible modules.
  foreach ($file->requires as $requires) {
    if (isset($files[$requires]) && _system_is_incompatible($incompatible, $files, $files[$requires])) {
773 774 775 776 777 778
      $incompatible[$file->name] = TRUE;
      return TRUE;
    }
  }
}

779 780 781
/**
 * Menu callback; provides module enable/disable interface.
 *
782 783 784 785
 * The list of modules gets populated by module.info.yml files, which contain
 * each module's name, description, and information about which modules it
 * requires.
 * See drupal_parse_info_file() for information on module.info.yml descriptors.
786
 *
787 788 789
 * Dependency checking is performed to ensure that a module:
 * - can not be enabled if there are disabled modules it requires.
 * - can not be disabled if there are enabled modules which depend on it.
790
 *
791 792
 * @param $form_state
 *   An associative array containing the current state of the form.
793 794 795 796
 *
 * @return
 *   The form array.
 *
797
 * @ingroup forms
798 799
 * @see theme_system_modules()
 * @see system_modules_submit()
800
 */
801
function system_modules($form, $form_state = array()) {
802
  // Get current list of modules.
803
  $files = system_rebuild_module_data();
804

805
  // Remove hidden modules from display list.
806 807
  $visible_files = $files;
  foreach ($visible_files as $filename => $file) {
808
    if (!empty($file->info['hidden'])) {
809
      unset($visible_files[$filename]);
810 811 812
    }
  }

813
  uasort($visible_files, 'system_sort_modules_by_info_name');
814

815
  // If the modules form was submitted, then system_modules_submit() runs first
816
  // and if there are unfilled required modules, then $form_state['storage'] is
817 818
  // filled, triggering a rebuild. In this case we need to display a
  // confirmation form.
819
  if (!empty($form_state['storage'])) {
820
    return system_modules_confirm_form($visible_files, $form_state['storage']);
821
  }
822

823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
  // JS-only table filters.
  $form['filters'] = array(
    '#type' => 'container',
    '#attributes' => array(
      'class' => array('table-filter', 'js-show'),
    ),
  );
  $form['filters']['text'] = array(
    '#type' => 'search',
    '#title' => t('Search'),
    '#size' => 30,
    '#placeholder' => t('Enter module name…'),
    '#attributes' => array(
      'class' => array('table-filter-text'),
      'data-table' => '#system-modules',
      'autocomplete' => 'off',
      'title' => t('Enter a part of the module name or description to filter by.'),
    ),
  );

843 844
  $modules = array();
  $form['modules'] = array('#tree' => TRUE);
845

846 847 848
  // Used when checking if module implements a help page.
  $help_arg = module_exists('help') ? drupal_help_arg() : FALSE;

849
  // Used when displaying modules that are required by the installation profile.
850
  require_once DRUPAL_ROOT . '/core/includes/install.inc';
851 852
  $distribution_name = check_plain(drupal_install_profile_distribution_name());

853
  // Iterate through each of the modules.
854
  foreach ($visible_files as $filename => $module) {
855 856
    $extra = array();
    $extra['enabled'] = (bool) $module->status;
857 858
    if (!empty($module->info['required'] )) {
      $extra['disabled'] = TRUE;
859
      $extra['required_by'][] = $distribution_name . (!empty($module->info['explanation']) ? ' ('. $module->info['explanation'] .')' : '');
860 861
    }

862 863 864 865
    // If this module requires other modules, add them to the array.
    foreach ($module->requires as $requires => $v) {
      if (!isset($files[$requires])) {
        $extra['requires'][$requires] = t('@module (<span class="admin-missing">missing</span>)', array('@module' => drupal_ucfirst($requires)));
866 867
        $extra['disabled'] = TRUE;
      }
868 869
      // Only display visible modules.
      elseif (isset($visible_files[$requires])) {
870
        $requires_name = $files[$requires]->info['name'];
871
        // Disable this module if it is incompatible with the dependency's version.
872 873 874 875 876 877
        if ($incompatible_version = drupal_check_incompatibility($v, str_replace(DRUPAL_CORE_COMPATIBILITY . '-', '', $files[$requires]->info['version']))) {
          $extra['requires'][$requires] = t('@module (<span class="admin-missing">incompatible with</span> version @version)', array(
            '@module' => $requires_name . $incompatible_version,
            '@version' => $files[$requires]->info['version'],
          ));
          $extra['disabled'] = TRUE;
878
        }
879 880 881 882 883 884 885 886
        // Disable this module if the dependency is incompatible with this
        // version of Drupal core.
        elseif ($files[$requires]->info['core'] != DRUPAL_CORE_COMPATIBILITY) {
          $extra['requires'][$requires] = t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', array(
            '@module' => $requires_name,
          ));
          $extra['disabled'] = TRUE;
        }
887
        elseif ($files[$requires]->status) {
888
          $extra['requires'][$requires] = t('@module', array('@module' => $requires_name));
889 890 891
        }
        else {
          $extra['requires'][$requires] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $requires_name));
892 893
        }
      }
894 895
    }
    // Generate link for module's help page, if there is one.
896
    if ($help_arg && $module->status && in_array($filename, module_implements('help'))) {
897
      if (module_invoke($filename, 'help', "admin/help#$filename", $help_arg)) {
898 899 900 901 902 903
        $extra['links']['help'] = array(
          '#type' => 'link',
          '#title' => t('Help'),
          '#href' => "admin/help/$filename",
          '#options' => array('attributes' => array('class' =>  array('module-link', 'module-link-help'), 'title' => t('Help'))),
        );
904 905
      }
    }
906 907 908 909 910
    // Generate link for module's permission, if the user has access to it.
    if ($module->status && user_access('administer permissions') && in_array($filename, module_implements('permission'))) {
      $extra['links']['permissions'] = array(
        '#type' => 'link',
        '#title' => t('Permissions'),
911
        '#href' => 'admin/people/permissions',
912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
        '#options' => array('fragment' => 'module-' . $filename, 'attributes' => array('class' => array('module-link', 'module-link-permissions'), 'title' => t('Configure permissions'))),
      );
    }
    // Generate link for module's configuration page, if the module provides
    // one.
    if ($module->status && isset($module->info['configure'])) {
      $configure_link = menu_get_item($module->info['configure']);
      if ($configure_link['access']) {
        $extra['links']['configure'] = array(
          '#type' => 'link',
          '#title' => t('Configure'),
          '#href' => $configure_link['href'],
          '#options' => array('attributes' => array('class' => array('module-link', 'module-link-configure'), 'title' => $configure_link['description'])),
        );
      }
    }

929 930 931
    // If this module is required by other modules, list those, and then make it
    // impossible to disable this one.
    foreach ($module->required_by as $required_by => $v) {
932
      // Hidden modules are unset already.
933
      if (isset($visible_files[$required_by])) {
934
        if ($files[$required_by]->status == 1 && $module->status == 1) {
935
          $extra['required_by'][] = t('@module', array('@module' => $files[$required_by]->info['name']));
936 937 938
          $extra['disabled'] = TRUE;
        }
        else {
939
          $extra['required_by'][] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$required_by]->info['name']));
940
        }
941 942
      }
    }
943
    $form['modules'][$module->info['package']][$filename] = _system_modules_build_row($module->info, $extra);
944
  }
945

946
  // Add basic information to the details.
947 948
  foreach (element_children($form['modules']) as $package) {
    $form['modules'][$package] += array(
949
      '#type' => 'details',
950
      '#title' => t($package),
951
      '#theme' => 'system_modules_details',
952
      '#header' => array(
953 954 955
        array('data' => t('<span class="element-invisible">Enabled</span>'), 'class' => array('checkbox')),
        array('data' => t('Name'), 'class' => array('name')),
        array('data' => t('Description'), 'class' => array('description', RESPONSIVE_PRIORITY_LOW)),
956
      ),
957
      // Ensure that the "Core" package comes first.
958
      '#weight' => $package == 'Core' ? -10 : NULL,
959
    );
960 961
  }

962
  // Lastly, sort all packages by title.
963 964
  uasort($form['modules'], 'element_sort_by_title');

965
  $form['#attached']['library'][] = array('system', 'drupal.system.modules');
966
  $form['actions'] = array('#type' => 'actions');
967
  $form['actions']['submit'] = array(
968 969 970
    '#type' => 'submit',
    '#value' => t('Save configuration'),
  );
971
  $form['#action'] = url('admin/modules/list/confirm');
972 973 974 975

  return $form;
}

976
/**
977
 * Array sorting callback; sorts modules or themes by their name.
978
 */
979 980 981 982
function system_sort_modules_by_info_name($a, $b) {
  return strcasecmp($a->info['name'], $b->info['name']);
}

983 984 985 986 987 988 989 990 991 992 993 994 995
/**
 * Array sorting callback; sorts modules or themes by their name.
 */
function system_sort_themes($a, $b) {
  if ($a->is_default) {
    return -1;
  }
  if ($b->is_default) {
    return 1;
  }
  return strcasecmp($a->info['name'], $b->info['name']);
}

996
/**
997
 * Build a table row for the system modules page.
998
 */
999 1000 1001
function _system_modules_build_row($info, $extra) {
  // Add in the defaults.
  $extra += array(
1002 1003
    'requires' => array(),
    'required_by' => array(),
1004 1005
    'disabled' => FALSE,
    'enabled' => FALSE,
1006
    'links' => array(),