diff --git a/core/includes/common.inc b/core/includes/common.inc index 84cfa94..e4c9505 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1631,8 +1631,8 @@ function drupal_http_header_attributes(array $attributes = array()) { * This keeps the context of the link title ('settings' in the example) for * translators. * - * @param string $text - * The translated link text for the anchor tag. + * @param string|array $text + * The link text for the anchor tag as a translated string or render array. * @param string $path * The internal path or external URL being linked to, such as "node/34" or * "http://example.com/foo". After the url() function is called to construct @@ -1665,10 +1665,9 @@ function drupal_http_header_attributes(array $attributes = array()) { * @see theme_link() */ function l($text, $path, array $options = array()) { - // Build a variables array to keep the structure of the alter consistent with - // theme_link(). + // Start building a structured representation of our link to be altered later. $variables = array( - 'text' => $text, + 'text' => is_array($text) ? drupal_render($text) : $text, 'path' => $path, 'options' => $options, ); diff --git a/core/includes/errors.inc b/core/includes/errors.inc index f581599..3fe23b7 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -136,7 +136,9 @@ function _drupal_decode_exception($exception) { * An error message. */ function _drupal_render_exception_safe($exception) { - return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception))); + $decode = _drupal_decode_exception($exception); + unset($decode['backtrace']); + return check_plain(strtr('%type: !message in %function (line %line of %file).', $decode)); } /** diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 9b95a47..395e314 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -266,13 +266,6 @@ function install_begin_request(&$install_state) { if (!$install_state['interactive']) { drupal_override_server_variables($install_state['server']); } - // The user agent header is used to pass a database prefix in the request when - // running tests. However, for security reasons, it is imperative that no - // installation be permitted using such a prefix. - elseif (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) { - header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); - exit; - } // Initialize conf_path(). // This primes the site path to be used during installation. By not requiring @@ -282,6 +275,17 @@ function install_begin_request(&$install_state) { drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); + // If the hash salt leaks, it becomes possible to forge a valid testing user + // agent, install a new copy of Drupal, and take over the original site. To + // avoid this yet allow for automated testing of the installer, make sure + // there is also a special test-specific settings.php overriding conf_path(). + // _drupal_load_test_overrides() sets the simpletest_conf_path in-memory + // setting in this case. + if ($install_state['interactive'] && drupal_valid_test_ua() && !settings()->get('simpletest_conf_path')) { + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + exit; + } + // A request object from the HTTPFoundation to tell us about the request. $request = Request::createFromGlobals(); @@ -1079,6 +1083,12 @@ function install_settings_form($form, &$form_state, &$install_state) { function install_settings_form_validate($form, &$form_state) { $driver = $form_state['values']['driver']; $database = $form_state['values'][$driver]; + // When testing the interactive installer, copy the database password and + // the test prefix. + if ($test_prefix = drupal_valid_test_ua()) { + $database['prefix'] = $test_prefix; + $database['password'] = $GLOBALS['databases']['default']['default']['password']; + } $drivers = drupal_get_database_types(); $reflection = new \ReflectionClass($drivers[$driver]); $install_namespace = $reflection->getNamespaceName(); @@ -1086,9 +1096,11 @@ function install_settings_form_validate($form, &$form_state) { $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\')); $database['driver'] = $driver; - // TODO: remove when PIFR will be updated to use 'db_prefix' instead of - // 'prefix' in the database settings form. - $database['prefix'] = $database['db_prefix']; + // @todo PIFR uses 'db_prefix' instead of 'prefix'. Remove this when it gets + // fixed. + if (!$test_prefix) { + $database['prefix'] = $database['db_prefix']; + } unset($database['db_prefix']); $form_state['storage']['database'] = $database; @@ -1148,14 +1160,35 @@ function install_settings_form_submit($form, &$form_state) { global $install_state; // Update global settings array and save. - $settings['databases'] = (object) array( - 'value' => array('default' => array('default' => $form_state['storage']['database'])), - 'required' => TRUE, - ); - $settings['drupal_hash_salt'] = (object) array( - 'value' => Crypt::randomStringHashed(55), - 'required' => TRUE, - ); + $settings = array(); + $database = $form_state['storage']['database']; + // Ideally, there is no difference between the code executed by the + // automated test browser and an ordinary browser. However, the database + // settings need a different format and also need to skip the password + // when testing. The hash salt also needs to be skipped because the original + // salt is used to verify the validity of the automated test browser. + // Because of these, there's a little difference in the code following but + // it is small and self-contained. + if ($test_prefix = drupal_valid_test_ua()) { + foreach ($form_state['storage']['database'] as $k => $v) { + if ($k != 'password') { + $settings['databases']['default']['default'][$k] = (object) array( + 'value' => $v, + 'required' => TRUE, + ); + } + } + } + else { + $settings['databases']['default']['default'] = (object) array( + 'value' => $database, + 'required' => TRUE, + ); + $settings['drupal_hash_salt'] = (object) array( + 'value' => Crypt::randomStringHashed(55), + 'required' => TRUE, + ); + } drupal_rewrite_settings($settings); diff --git a/core/includes/install.inc b/core/includes/install.inc index 84dd851..5111916 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -95,7 +95,7 @@ function drupal_install_profile_distribution_name() { // installation state (it might not be saved anywhere yet). if (drupal_installation_attempted()) { global $install_state; - return $install_state['profile_info']['distribution_name']; + return isset($install_state['profile_info']['distribution_name']) ? $install_state['profile_info']['distribution_name'] : 'Drupal'; } // At all other times, we load the profile via standard methods. else { diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 011db8d..b4a06a9 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -387,6 +387,45 @@ function _theme_save_registry($theme, $registry) { } /** + * Adds mobile friendly meta tags to the head of the HTML page. + */ +function _theme_add_mobile_meta_tags() { + $elements = array( + 'MobileOptimized' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'MobileOptimized', + 'content' => 'width', + ), + ), + 'HandheldFriendly' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'HandheldFriendly', + 'content' => 'true', + ), + ), + 'viewport' => array( + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'viewport', + 'content' => 'width=device-width', + ), + ), + 'cleartype' => array( + '#tag' => 'meta', + '#attributes' => array( + 'http-equiv' => 'cleartype', + 'content' => 'on', + ), + ), + ); + foreach ($elements as $name => $element) { + drupal_add_html_head($element, $name); + } +} + +/** * Forces the system to rebuild the theme registry. * * This function should be called when modules are added to the system, or when @@ -1673,14 +1712,12 @@ function theme_status_messages($variables) { * * @param $variables * An associative array containing the keys 'text', 'path', and 'options'. - * See the l() function for information about these variables. However, unlike - * 'text' in l(), both render arrays and strings are supported here. + * See the l() function for information about these variables. * * @see l() */ function theme_link($variables) { - $rendered_text = is_array($variables['text']) ? drupal_render($variables['text']) : $variables['text']; - return l($rendered_text, $variables['path'], $variables['options']); + return l($variables['text'], $variables['path'], $variables['options']); } /** @@ -2737,40 +2774,8 @@ function template_preprocess_html(&$variables) { $variables['head_title_array'] = $head_title; $variables['head_title'] = implode(' | ', $head_title); - // Display the html.tpl.php's default mobile metatags for responsive design. - $elements = array( - 'MobileOptimized' => array( - '#tag' => 'meta', - '#attributes' => array( - 'name' => 'MobileOptimized', - 'content' => 'width', - ), - ), - 'HandheldFriendly' => array( - '#tag' => 'meta', - '#attributes' => array( - 'name' => 'HandheldFriendly', - 'content' => 'true', - ), - ), - 'viewport' => array( - '#tag' => 'meta', - '#attributes' => array( - 'name' => 'viewport', - 'content' => 'width=device-width', - ), - ), - 'cleartype' => array( - '#tag' => 'meta', - '#attributes' => array( - 'http-equiv' => 'cleartype', - 'content' => 'on', - ), - ), - ); - foreach ($elements as $name => $element) { - drupal_add_html_head($element, $name); - } + // Display the default mobile metatags for responsive design. + _theme_add_mobile_meta_tags(); // Populate the page template suggestions. if ($suggestions = theme_get_suggestions(arg(), 'html')) { @@ -2783,9 +2788,12 @@ function template_preprocess_html(&$variables) { * * Default template: page.html.twig. * - * Most themes utilize their own copy of page.html.twig. The default is located - * inside "modules/system/page.html.twig". Look in there for the full list of - * variables. + * @param array $variables + * An associative array. + * + * Most themes provide their own copy of page.html.twig. The default is located + * inside "modules/system/templates/page.html.twig". Look in there for the full + * list of variables. * * Uses the arg() function to generate a series of page template suggestions * based on the current path. @@ -3044,7 +3052,7 @@ function template_preprocess_maintenance_page(&$variables) { $site_name = $site_config->get('name'); $site_slogan = $site_config->get('slogan'); - // Construct page title + // Construct page title. if (drupal_get_title()) { $head_title = array( 'title' => strip_tags(drupal_get_title()), @@ -3093,6 +3101,9 @@ function template_preprocess_maintenance_page(&$variables) { $variables['attributes']['class'][] = 'sidebar-' . $variables['layout']; } + // Display the default mobile metatags for responsive design. + _theme_add_mobile_meta_tags(); + // Dead databases will show error messages so supplying this template will // allow themers to override the page and the content completely. if (isset($variables['db_is_active']) && !$variables['db_is_active']) { @@ -3124,14 +3135,44 @@ function template_process_maintenance_page(&$variables) { } /** - * Preprocess variables for region.tpl.php + * Prepare variables for install page templates. + * + * Default template: install-page.html.twig + * + * @param array $variables + * An associative array containing: + * - @todo * - * Prepares the values passed to the theme_region function to be passed into a - * pluggable template engine. Uses the region name to generate a template file - * suggestions. If none are found, the default region.tpl.php is used. + * @see template_preprocess_maintenance_page() + */ +function template_preprocess_install_page(&$variables) { + template_preprocess_maintenance_page($variables); + // Override the site name that is displayed on the page, since Drupal is + // still in the process of being installed. + $variables['site_name'] = drupal_install_profile_distribution_name(); +} + +/** + * Preprocess variables for install-page.tpl.php. + * + * @see install-page.html.twig + * @see template_process_html() + * @todo Remove this function. + */ +function template_process_install_page(&$variables) { + template_process_maintenance_page($variables); +} + +/** + * Prepare variables for region templates. + * + * Default template: region.html.twig + * + * @param array $variables + * An associative array containing: + * - elements: @todo * * @see drupal_region_class() - * @see region.tpl.php */ function template_preprocess_region(&$variables) { // Create the $content variable that templates expect. @@ -3234,7 +3275,8 @@ function drupal_common_theme() { 'template' => 'maintenance-page', ), 'install_page' => array( - 'variables' => array('content' => NULL), + 'variables' => array('content' => NULL, 'show_messages' => TRUE), + 'template' => 'install-page', ), 'task_list' => array( 'variables' => array('items' => NULL, 'active' => NULL), diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 9215957..1001ba1 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -148,20 +148,6 @@ function theme_task_list($variables) { } /** - * Returns HTML for the installation page. - * - * Note: this function is not themeable. - * - * @param $variables - * An associative array containing: - * - content: The page content to show. - */ -function theme_install_page($variables) { - drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); - return theme('maintenance_page', $variables); -} - -/** * Returns HTML for a results report of an operation run by authorize.php. * * @param $variables diff --git a/core/lib/Drupal/Core/SystemListingInfo.php b/core/lib/Drupal/Core/SystemListingInfo.php index b2e42e1..989b0f2 100644 --- a/core/lib/Drupal/Core/SystemListingInfo.php +++ b/core/lib/Drupal/Core/SystemListingInfo.php @@ -28,7 +28,7 @@ protected function profiles($directory) { // For SimpleTest to be able to test modules packaged together with a // distribution we need to include the profile of the parent site (in // which test runs are triggered). - if (drupal_valid_test_ua()) { + if (drupal_valid_test_ua() && !drupal_installation_attempted()) { $testing_profile = config('simpletest.settings')->get('parent_profile'); if ($testing_profile && $testing_profile != $profile) { $searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory; diff --git a/core/modules/block/block.admin.inc b/core/modules/block/block.admin.inc index 78df3fe..10927bd 100644 --- a/core/modules/block/block.admin.inc +++ b/core/modules/block/block.admin.inc @@ -14,8 +14,11 @@ * @see block_menu() */ function block_admin_demo($theme = NULL) { - drupal_add_css(drupal_get_path('module', 'block') . '/block.admin.css'); - return ''; + return array( + '#attached' => array( + 'css' => array(drupal_get_path('module', 'block') . '/block.admin.css'), + ), + ); } /** diff --git a/core/modules/help/lib/Drupal/help/Controller/HelpController.php b/core/modules/help/lib/Drupal/help/Controller/HelpController.php index b8dc6d9..2038b8d 100644 --- a/core/modules/help/lib/Drupal/help/Controller/HelpController.php +++ b/core/modules/help/lib/Drupal/help/Controller/HelpController.php @@ -46,9 +46,12 @@ public static function create(ContainerInterface $container) { * An HTML string representing the contents of help page. */ public function helpMain() { - // Add CSS. - drupal_add_css(drupal_get_path('module', 'help') . '/help.css'); - $output = '
' . t('Help is available on the following items:') . '
' . $this->helpLinksAsList(); + $output = array( + '#attached' => array( + 'css' => array(drupal_get_path('module', 'help') . '/help.css'), + ), + '#markup' => '' . t('Help is available on the following items:') . '
' . $this->helpLinksAsList(), + ); return $output; } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php index a4641f6..7631016 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php @@ -64,12 +64,9 @@ function testInternalBrowser() { ))); $this->assertNoTitle('Foo'); - // Make sure that we are locked out of the installer when prefixing - // using the user-agent header. This is an important security check. global $base_url; - - $this->drupalGet($base_url . '/core/install.php', array('external' => TRUE)); - $this->assertResponse(403, 'Cannot access install.php with a "simpletest" user-agent header.'); + $this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE))); + $this->assertResponse(403, 'Cannot access install.php.'); $user = $this->drupalCreateUser(); $this->drupalLogin($user); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index a75ade4..a2bb55a 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1385,9 +1385,9 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); } if (!$ajax && isset($submit)) { - $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit))); + $this->assertTrue($submit_matches, format_string('Found the @submit button', array('@submit' => $submit))); } - $this->fail(t('Found the requested form fields at @path', array('@path' => $path))); + $this->fail(format_string('Found the requested form fields at @path', array('@path' => $path))); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php index a241a31..a502e82 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php @@ -134,12 +134,17 @@ function testLinkCustomClass() { } /** - * Tests that theme_link() supports render arrays in 'text' parameter. + * Tests that link functions support render arrays as 'text'. */ - function testLinkNestedRenderArrays() { + function testLinkRenderArrayText() { // Build a link with l() for reference. $l = l('foo', 'http://drupal.org'); + // Test a renderable array passed to l(). + $renderable_text = array('#markup' => 'foo'); + $l_renderable_text = l($renderable_text, 'http://drupal.org'); + $this->assertEqual($l_renderable_text, $l); + // Test a themed link with plain text 'text'. $theme_link_plain_array = array( '#theme' => 'link', diff --git a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php new file mode 100644 index 0000000..43824bf --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php @@ -0,0 +1,156 @@ + 'Installer tests', + 'description' => 'Tests the interactive installer.', + 'group' => 'Installer', + ); + } + + protected function setUp() { + global $conf; + + // When running tests through the SimpleTest UI (vs. on the command line), + // SimpleTest's batch conflicts with the installer's batch. Batch API does + // not support the concept of nested batches (in which the nested is not + // progressive), so we need to temporarily pretend there was no batch. + // Back up the currently running SimpleTest batch. + $this->originalBatch = batch_get(); + + // Create the database prefix for this test. + $this->prepareDatabasePrefix(); + + // Prepare the environment for running tests. + $this->prepareEnvironment(); + if (!$this->setupEnvironment) { + return FALSE; + } + + // Reset all statics and variables to perform tests in a clean environment. + $conf = array(); + drupal_static_reset(); + + // Change the database prefix. + // All static variables need to be reset before the database prefix is + // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to + // write back to persistent caches when they are destructed. + $this->changeDatabasePrefix(); + if (!$this->setupDatabasePrefix) { + return FALSE; + } + $variable_groups = array( + 'system.file' => array( + 'path.private' => $this->private_files_directory, + 'path.temporary' => $this->temp_files_directory, + ), + 'locale.settings' => array( + 'translation.path' => $this->translation_files_directory, + ), + ); + foreach ($variable_groups as $config_base => $variables) { + foreach ($variables as $name => $value) { + NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value); + } + } + $settings['conf_path'] = (object) array( + 'value' => $this->public_files_directory, + 'required' => TRUE, + ); + $settings['config_directories'] = (object) array( + 'value' => array(), + 'required' => TRUE, + ); + $this->writeSettings($settings); + + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=minimal'); + $this->drupalPost(NULL, array(), 'Save and continue'); + // Reload config directories. + include $this->public_files_directory . '/settings.php'; + $prefix = substr($this->public_files_directory, strlen(conf_path() . '/files/')); + foreach ($config_directories as $type => $data) { + $GLOBALS['config_directories'][$type]['path'] = $prefix . '/files/' . $data['path']; + } + $this->rebuildContainer(); + + foreach ($variable_groups as $config_base => $variables) { + $config = config($config_base); + foreach ($variables as $name => $value) { + $config->set($name, $value); + } + $config->save(); + } + + // Use the test mail class instead of the default mail handler class. + config('system.mail')->set('interface.default', 'Drupal\Core\Mail\VariableLog')->save(); + + drupal_set_time_limit($this->timeLimit); + // When running from run-tests.sh we don't get an empty current path which + // would indicate we're on the home page. + $path = current_path(); + if (empty($path)) { + _current_path('run-tests'); + } + $this->setup = TRUE; + } + + /** + * {@inheritdoc} + * + * During setup(), drupalPost calls refreshVariables() which tries to read + * variables which are not yet there because the child Drupal is not yet + * installed. + */ + protected function refreshVariables() { + if (!empty($this->setup)) { + parent::refreshVariables(); + } + } + + /** + * {@inheritdoc} + * + * This override is necessary because the parent drupalGet() calls t(), which + * is not available early during installation. + */ + protected function drupalGet($path, array $options = array(), array $headers = array()) { + // We are re-using a CURL connection here. If that connection still has + // certain options set, it might change the GET into a POST. Make sure we + // clear out previous options. + $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $this->getAbsoluteUrl($path), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); + $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + + // Replace original page output with new output from redirected page(s). + if ($new = $this->checkForMetaRefresh()) { + $out = $new; + } + $this->verbose('GET request to: ' . $path . + '