diff --git a/core/includes/language.inc b/core/includes/language.inc index 74d5752..11b71c9 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -13,6 +13,11 @@ const LANGUAGE_NEGOTIATION_SELECTED = 'language-selected'; /** + * The language is determined using the current interface language. + */ +const LANGUAGE_NEGOTIATION_INTERFACE = 'language-interface'; + +/** * @defgroup language_negotiation Language Negotiation API functionality * @{ * Functions to customize the language types and the negotiation process. @@ -49,6 +54,7 @@ * unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']); * } * @endcode + * @todo Fix documentation with the locked parameter. * * Every language type can have a different set of language negotiation methods * assigned to it. Different language types often share the same language @@ -165,34 +171,15 @@ function language_types_info() { * whose language negotiation methods are module-defined and not altered through * the user interface. * - * @param $stored - * (optional) By default, retrieves values from the 'language_types' variable - * to avoid unnecessary hook invocations. If set to FALSE, retrieves values - * from the actual language type definitions. This allows reaction to - * alterations performed on the definitions by modules installed after the - * 'language_types' variable is set. - * * @return * An array of language type names. */ -function language_types_get_configurable($stored = TRUE) { +function language_types_get_configurable() { $configurable = &drupal_static(__FUNCTION__); - - if ($stored && !isset($configurable)) { - $types = variable_get('language_types', language_types_get_default()); - $configurable = array_keys(array_filter($types)); + if (!isset($configurable)) { + $configurable = config('system.language.types')->get('configurable'); + $configurable = empty($configurable) ? array() : array_keys(array_filter($configurable)); } - - if (!$stored) { - $result = array(); - foreach (language_types_info() as $type => $info) { - if (!isset($info['fixed'])) { - $result[] = $type; - } - } - return $result; - } - return $configurable; } @@ -214,37 +201,62 @@ function language_types_disable($types) { /** * Updates the language type configuration. + * + * @param array $configurable + * The configuration object containing the user defined preferences for + * language type customizability. + * @todo + * We are using variable_set('language_types', $language_types) and + * config('system.language.types')->set('configurable', $configurable)->save() + * at the same time. This is redundant. We need to change variable_set to + * configuration. */ -function language_types_set() { +function language_types_set($configurable) { // Ensure that we are getting the defined language negotiation information. An // invocation of module_enable() or module_disable() could outdate the cached // information. drupal_static_reset('language_types_info'); drupal_static_reset('language_negotiation_info'); - // Determine which language types are configurable and which not by checking - // whether the 'fixed' key is defined. Non-configurable (fixed) language types - // have their language negotiation settings stored there. $language_types = array(); $negotiation_info = language_negotiation_info(); foreach (language_types_info() as $type => $info) { - if (isset($info['fixed'])) { - $language_types[$type] = FALSE; - $method_weights = array(); - foreach ($info['fixed'] as $weight => $method_id) { - if (isset($negotiation_info[$method_id])) { - $method_weights[$method_id] = $weight; + // Check if the language is locked. The configuration of a locked language + // type cannot be changed using the UI. + if ($locked = !empty($info['locked'])) { + // If a locked language has default settings (by using the 'fixed' + // property) it will be always configurable. If it has no default + // settings, then it won't be configurable. + $configurable[$type] = empty($info['fixed']); + $language_types[$type] = !$locked; + if (!$configurable[$type]) { + $method_weights = array(); + foreach ($info['fixed'] as $weight => $method_id) { + if (isset($negotiation_info[$method_id])) { + $method_weights[$method_id] = $weight; + } } + language_negotiation_set($type, $method_weights); } - language_negotiation_set($type, $method_weights); } else { - $language_types[$type] = TRUE; + // The configuration of an unlocked language will be based on the + // $configurable array (stored in configuration). These values may be + // altered using the UI. + $language_types[$type] = !empty($configurable[$type]); + if (!$language_types[$type] && empty($info['fixed'])) { + // If the language is not configurable and there is no default language + // negotiation setting then provide one. + $method_weights = array(LANGUAGE_NEGOTIATION_INTERFACE); + $method_weights = array_flip($method_weights); + language_negotiation_set($type, $method_weights); + } } } // Save enabled language types. variable_set('language_types', $language_types); + config('system.language.types')->set('configurable', $configurable)->save(); // Ensure that subsequent calls of language_types_get_configurable() return // the updated language type information. @@ -365,7 +377,7 @@ function language_negotiation_set($type, $method_weights) { $negotiation = array(); $negotiation_info = language_negotiation_info(); - $default_types = language_types_get_configurable(FALSE); + $default_types = language_types_get_configurable(); // Order the language negotiation method list by weight. asort($method_weights); diff --git a/core/modules/aggregator/aggregator.admin.inc b/core/modules/aggregator/aggregator.admin.inc index 9b8d4ff..e7e2300 100644 --- a/core/modules/aggregator/aggregator.admin.inc +++ b/core/modules/aggregator/aggregator.admin.inc @@ -7,6 +7,8 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Drupal\aggregator\Plugin\Core\Entity\Feed; +use Guzzle\Http\Exception\RequestException; +use Guzzle\Http\Exception\BadResponseException; /** * Page callback: Displays the aggregator administration page. @@ -96,6 +98,176 @@ function aggregator_view() { } /** + * Form constructor for importing feeds from OPML. + * + * @ingroup forms + * @see aggregator_menu() + * @see aggregator_form_opml_validate() + * @see aggregator_form_opml_submit() + */ +function aggregator_form_opml($form, &$form_state) { + $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval'); + + $form['upload'] = array( + '#type' => 'file', + '#title' => t('OPML File'), + '#description' => t('Upload an OPML file containing a list of feeds to be imported.'), + ); + $form['remote'] = array( + '#type' => 'url', + '#title' => t('OPML Remote URL'), + '#maxlength' => 1024, + '#description' => t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'), + ); + $form['refresh'] = array( + '#type' => 'select', + '#title' => t('Update interval'), + '#default_value' => 3600, + '#options' => $period, + '#description' => t('The length of time between feed updates. Requires a correctly configured cron maintenance task.', array('@cron' => url('admin/reports/status'))), + ); + $form['block'] = array('#type' => 'select', + '#title' => t('News items in block'), + '#default_value' => 5, + '#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)), + '#description' => t("Drupal can make a block with the most recent news items of a feed. You can configure blocks to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in a feed's block. If you choose '0' these feeds' blocks will be disabled.", array('@block-admin' => url('admin/structure/block'))), + ); + + // Handling of categories. + $options = array_map('check_plain', db_query("SELECT cid, title FROM {aggregator_category} ORDER BY title")->fetchAllKeyed()); + if ($options) { + $form['category'] = array( + '#type' => 'checkboxes', + '#title' => t('Categorize news items'), + '#options' => $options, + '#description' => t('New feed items are automatically filed in the checked categories.'), + ); + } + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Import') + ); + + return $form; +} + +/** + * Form validation handler for aggregator_form_opml(). + * + * @see aggregator_form_opml_submit() + */ +function aggregator_form_opml_validate($form, &$form_state) { + // If both fields are empty or filled, cancel. + if (empty($form_state['values']['remote']) == empty($_FILES['files']['name']['upload'])) { + form_set_error('remote', t('You must either upload a file or enter a URL.')); + } +} + +/** + * Form submission handler for aggregator_form_opml(). + * + * @see aggregator_form_opml_validate() + */ +function aggregator_form_opml_submit($form, &$form_state) { + $data = ''; + $validators = array('file_validate_extensions' => array('opml xml')); + if ($file = file_save_upload('upload', $validators, FALSE, 0)) { + $data = file_get_contents($file->uri); + } + else { + try { + $response = Drupal::httpClient() + ->get($form_state['values']['remote']) + ->send(); + $data = $response->getBody(TRUE); + } + catch (BadResponseException $e) { + $response = $e->getResponse(); + watchdog('aggregator', 'Failed to download OPML file due to "%error".', array('%error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()), WATCHDOG_WARNING); + drupal_set_message(t('Failed to download OPML file due to "%error".', array('%error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()))); + return; + } + catch (RequestException $e) { + watchdog('aggregator', 'Failed to download OPML file due to "%error".', array('%error' => $e->getMessage()), WATCHDOG_WARNING); + drupal_set_message(t('Failed to download OPML file due to "%error".', array('%error' => $e->getMessage()))); + return; + } + } + + $feeds = _aggregator_parse_opml($data); + if (empty($feeds)) { + drupal_set_message(t('No new feed has been added.')); + return; + } + + foreach ($feeds as $feed) { + // Ensure URL is valid. + if (!valid_url($feed['url'], TRUE)) { + drupal_set_message(t('The URL %url is invalid.', array('%url' => $feed['url'])), 'warning'); + continue; + } + + // Check for duplicate titles or URLs. + $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = :title OR url = :url", array(':title' => $feed['title'], ':url' => $feed['url'])); + foreach ($result as $old) { + if (strcasecmp($old->title, $feed['title']) == 0) { + drupal_set_message(t('A feed named %title already exists.', array('%title' => $old->title)), 'warning'); + continue 2; + } + if (strcasecmp($old->url, $feed['url']) == 0) { + drupal_set_message(t('A feed with the URL %url already exists.', array('%url' => $old->url)), 'warning'); + continue 2; + } + } + + $new_feed = entity_create('aggregator_feed', array( + 'title' => $feed['title'], + 'url' => $feed['url'], + 'refresh' => $form_state['values']['refresh'], + 'block' => $form_state['values']['block'], + )); + $new_feed->categories = $form_state['values']['category']; + $new_feed->save(); + } + + $form_state['redirect'] = 'admin/config/services/aggregator'; +} + +/** + * Parses an OPML file. + * + * Feeds are recognized as elements with the attributes "text" and + * "xmlurl" set. + * + * @param $opml + * The complete contents of an OPML document. + * + * @return + * An array of feeds, each an associative array with a "title" and a "url" + * element, or NULL if the OPML document failed to be parsed. An empty array + * will be returned if the document is valid but contains no feeds, as some + * OPML documents do. + */ +function _aggregator_parse_opml($opml) { + $feeds = array(); + $xml_parser = drupal_xml_parser_create($opml); + if (xml_parse_into_struct($xml_parser, $opml, $values)) { + foreach ($values as $entry) { + if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) { + $item = $entry['attributes']; + if (!empty($item['XMLURL']) && !empty($item['TEXT'])) { + $feeds[] = array('title' => $item['TEXT'], 'url' => $item['XMLURL']); + } + } + } + } + xml_parser_free($xml_parser); + + return $feeds; +} + +/** * Page callback: Refreshes a feed, then redirects to the overview page. * * @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module index 9755428..b544908 100644 --- a/core/modules/aggregator/aggregator.module +++ b/core/modules/aggregator/aggregator.module @@ -112,8 +112,11 @@ function aggregator_menu() { ); $items['admin/config/services/aggregator/add/opml'] = array( 'title' => 'Import OPML', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('aggregator_form_opml'), + 'access arguments' => array('administer news feeds'), 'type' => MENU_LOCAL_ACTION, - 'route_name' => 'aggregator_opml_add', + 'file' => 'aggregator.admin.inc', ); $items['admin/config/services/aggregator/remove/%aggregator_feed'] = array( 'title' => 'Remove items', diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml index d4f1f6c..bf02d5c 100644 --- a/core/modules/aggregator/aggregator.routing.yml +++ b/core/modules/aggregator/aggregator.routing.yml @@ -25,10 +25,3 @@ aggregator_feed_add: _controller: '\Drupal\aggregator\Routing\AggregatorController::feedAdd' requirements: _permission: 'administer news feeds' - -aggregator_opml_add: - pattern: '/admin/config/services/aggregator/add/opml' - defaults: - _form: '\Drupal\aggregator\Form\OpmlFeedAdd' - requirements: - _permission: 'administer news feeds' diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Form/OpmlFeedAdd.php b/core/modules/aggregator/lib/Drupal/aggregator/Form/OpmlFeedAdd.php deleted file mode 100644 index 7e1121e..0000000 --- a/core/modules/aggregator/lib/Drupal/aggregator/Form/OpmlFeedAdd.php +++ /dev/null @@ -1,267 +0,0 @@ -database = $database; - $this->queryFactory = $query_factory; - $this->entityManager = $entity_manager; - $this->httpClient = $http_client; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('database'), - $container->get('entity.query'), - $container->get('plugin.manager.entity'), - $container->get('http_default_client') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormID() { - return 'aggregator_opml_add'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, array &$form_state) { - $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, - 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval'); - - $form['upload'] = array( - '#type' => 'file', - '#title' => t('OPML File'), - '#description' => t('Upload an OPML file containing a list of feeds to be imported.'), - ); - $form['remote'] = array( - '#type' => 'url', - '#title' => t('OPML Remote URL'), - '#maxlength' => 1024, - '#description' => t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'), - ); - $form['refresh'] = array( - '#type' => 'select', - '#title' => t('Update interval'), - '#default_value' => 3600, - '#options' => $period, - '#description' => t('The length of time between feed updates. Requires a correctly configured cron maintenance task.', array('@cron' => url('admin/reports/status'))), - ); - $form['block'] = array( - '#type' => 'select', - '#title' => t('News items in block'), - '#default_value' => 5, - '#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)), - '#description' => t("Drupal can make a block with the most recent news items of a feed. You can configure blocks to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in a feed's block. If you choose '0' these feeds' blocks will be disabled.", array('@block-admin' => url('admin/structure/block'))), - ); - - // Handling of categories. - $options = array_map('check_plain', $this->database->query("SELECT cid, title FROM {aggregator_category} ORDER BY title")->fetchAllKeyed()); - if ($options) { - $form['category'] = array( - '#type' => 'checkboxes', - '#title' => t('Categorize news items'), - '#options' => $options, - '#description' => t('New feed items are automatically filed in the checked categories.'), - ); - } - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Import'), - ); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, array &$form_state) { - // If both fields are empty or filled, cancel. - if (empty($form_state['values']['remote']) == empty($_FILES['files']['name']['upload'])) { - form_set_error('remote', t('You must either upload a file or enter a URL.')); - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, array &$form_state) { - $data = ''; - $validators = array('file_validate_extensions' => array('opml xml')); - if ($file = file_save_upload('upload', $validators, FALSE, 0)) { - $data = file_get_contents($file->uri); - } - else { - // @todo Move this to a fetcher implementation. - try { - $response = $this->httpClient->get($form_state['values']['remote'])->send(); - $data = $response->getBody(TRUE); - } - catch (BadResponseException $e) { - $response = $e->getResponse(); - watchdog('aggregator', 'Failed to download OPML file due to "%error".', array('%error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()), WATCHDOG_WARNING); - drupal_set_message(t('Failed to download OPML file due to "%error".', array('%error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()))); - return; - } - catch (RequestException $e) { - watchdog('aggregator', 'Failed to download OPML file due to "%error".', array('%error' => $e->getMessage()), WATCHDOG_WARNING); - drupal_set_message(t('Failed to download OPML file due to "%error".', array('%error' => $e->getMessage()))); - return; - } - } - - $feeds = $this->parseOpml($data); - if (empty($feeds)) { - drupal_set_message(t('No new feed has been added.')); - return; - } - - // @todo Move this functionality to a processor. - foreach ($feeds as $feed) { - // Ensure URL is valid. - if (!valid_url($feed['url'], TRUE)) { - drupal_set_message(t('The URL %url is invalid.', array('%url' => $feed['url'])), 'warning'); - continue; - } - - // Check for duplicate titles or URLs. - $query = $this->queryFactory->get('aggregator_feed'); - $condition = $query->orConditionGroup() - ->condition('title', $feed['title']) - ->condition('url', $feed['url']); - $ids = $query - ->condition($condition) - ->execute(); - $result = $this->entityManager - ->getStorageController('aggregator_feed') - ->load($ids); - foreach ($result as $old) { - if (strcasecmp($old->label(), $feed['title']) == 0) { - drupal_set_message(t('A feed named %title already exists.', array('%title' => $old->label())), 'warning'); - continue 2; - } - if (strcasecmp($old->url->value, $feed['url']) == 0) { - drupal_set_message(t('A feed with the URL %url already exists.', array('%url' => $old->url->value)), 'warning'); - continue 2; - } - } - - $new_feed = $this->entityManager - ->getStorageController('aggregator_feed') - ->create(array( - 'title' => $feed['title'], - 'url' => $feed['url'], - 'refresh' => $form_state['values']['refresh'], - 'block' => $form_state['values']['block'], - )); - $new_feed->categories = $form_state['values']['category']; - $new_feed->save(); - } - - $form_state['redirect'] = 'admin/config/services/aggregator'; - } - - /** - * Parses an OPML file. - * - * Feeds are recognized as elements with the attributes "text" and - * "xmlurl" set. - * - * @todo Move this functionality to a parser. - * - * @param $opml - * The complete contents of an OPML document. - * - * @return - * An array of feeds, each an associative array with a "title" and a "url" - * element, or NULL if the OPML document failed to be parsed. An empty array - * will be returned if the document is valid but contains no feeds, as some - * OPML documents do. - */ - protected function parseOpml($opml) { - $feeds = array(); - $xml_parser = drupal_xml_parser_create($opml); - if (xml_parse_into_struct($xml_parser, $opml, $values)) { - foreach ($values as $entry) { - if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) { - $item = $entry['attributes']; - if (!empty($item['XMLURL']) && !empty($item['TEXT'])) { - $feeds[] = array('title' => $item['TEXT'], 'url' => $item['XMLURL']); - } - } - } - } - xml_parser_free($xml_parser); - - return $feeds; - } - -} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Tests/FeedParserTest.php b/core/modules/aggregator/lib/Drupal/aggregator/Tests/FeedParserTest.php index 5f4f9f9..caf2e7d 100644 --- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/FeedParserTest.php +++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/FeedParserTest.php @@ -100,6 +100,6 @@ function testInvalidFeed() { // Update the feed. Use the UI to be able to check the message easily. $this->drupalGet('admin/config/services/aggregator'); $this->clickLink(t('Update items')); - $this->assertRaw(t('The feed from %title seems to be broken because of error', array('%title' => $feed->label()))); + $this->assertRaw(t('The feed from %title seems to be broken because of error "%error"', array('%title' => $feed->label(), '%error' => "[curl] 6: Couldn't resolve host 'http' [url] /"))); } } diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc index f59fb38..b4d1b71 100644 --- a/core/modules/language/language.admin.inc +++ b/core/modules/language/language.admin.inc @@ -357,11 +357,17 @@ function language_negotiation_configure_form() { $form = array( '#submit' => array('language_negotiation_configure_form_submit'), '#theme' => 'language_negotiation_configure_form', - '#language_types' => language_types_get_configurable(FALSE), '#language_types_info' => language_types_info(), '#language_negotiation_info' => language_negotiation_info(), ); - + $form['#language_types'] = array(); + $configurable = config('system.language.types')->get('configurable'); + foreach ($form['#language_types_info'] as $type => $info) { + // Show locked language types only if they they are configurable. + if (empty($info['locked']) || $configurable[$type]) { + $form['#language_types'][] = $type; + } + } foreach ($form['#language_types'] as $type) { language_negotiation_configure_form_table($form, $type); } @@ -389,6 +395,21 @@ function language_negotiation_configure_form_table(&$form, $type) { '#show_operations' => FALSE, 'weight' => array('#tree' => TRUE), ); + // Only show configurability checkbox for the unlocked language types. + if (empty($info['locked'])) { + $configurable = config('system.language.types')->get('configurable'); + $table_form['configurable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize %language_name language detection to differ from User interface text language detection settings.', array('%language_name' => $info['name'])), + '#default_value' => !empty($configurable[$type]), + '#attributes' => array('class' => array('language-customization-checkbox')), + '#attached' => array( + 'library' => array( + array('language', 'language.admin') + ), + ), + ); + } $negotiation_info = $form['#language_negotiation_info']; $enabled_methods = variable_get("language_negotiation_$type", array()); @@ -519,12 +540,13 @@ function theme_language_negotiation_configure_form($variables) { 'rows' => $rows, 'attributes' => array('id' => "language-negotiation-methods-$type"), ); - $table = theme('table', $variables); + $table = drupal_render($form[$type]['configurable']); + $table .= theme('table', $variables); $table .= drupal_render_children($form[$type]); drupal_add_tabledrag("language-negotiation-methods-$type", 'order', 'sibling', "language-method-weight-$type"); - $output .= '
' . $title . $description . $table . '
'; + $output .= '
' . $title . $description . $table . '
'; } $output .= drupal_render_children($form); @@ -537,11 +559,16 @@ function theme_language_negotiation_configure_form($variables) { function language_negotiation_configure_form_submit($form, &$form_state) { $configurable_types = $form['#language_types']; + $configurable = config('system.language.types')->get('configurable'); + foreach ($configurable_types as $type) { $method_weights = array(); $enabled_methods = $form_state['values'][$type]['enabled']; $enabled_methods[LANGUAGE_NEGOTIATION_SELECTED] = TRUE; $method_weights_input = $form_state['values'][$type]['weight']; + if (isset($form_state['values'][$type]['configurable'])) { + $configurable[$type] = !empty($form_state['values'][$type]['configurable']); + } foreach ($method_weights_input as $method_id => $weight) { if ($enabled_methods[$method_id]) { @@ -555,7 +582,13 @@ function language_negotiation_configure_form_submit($form, &$form_state) { // Update non-configurable language types and the related language negotiation // configuration. - language_types_set(); + language_types_set($configurable); + + // Clear block definitions cache since the available blocks and their names + // may have been changed based on the configurable types. + if (module_exists('block')) { + drupal_container()->get('plugin.manager.block')->clearCachedDefinitions(); + } $form_state['redirect'] = 'admin/config/regional/language/detection'; drupal_set_message(t('Language negotiation configuration saved.')); diff --git a/core/modules/language/language.admin.js b/core/modules/language/language.admin.js new file mode 100644 index 0000000..dda9a65 --- /dev/null +++ b/core/modules/language/language.admin.js @@ -0,0 +1,32 @@ +(function ($, Drupal) { + +"use strict"; + +/** + * Makes language negotiation inherit user interface negotiation. + */ +Drupal.behaviors.negotiationLanguage = { + attach: function () { + var $configForm = $('#language-negotiation-configure-form'); + var inputSelector = 'input[name$="[configurable]"]'; + // Given a customization checkbox derive the language type being changed. + function toggleTable (checkbox) { + var $checkbox = $(checkbox); + // Get the language detection type such as User interface text language + // detection or Content language detection. + $checkbox.closest('.table-language-group') + .find('table, .tabledrag-toggle-weight') + .toggle($checkbox.prop('checked')); + } + // Bind hide/show + rearrange to customization checkboxes. + $configForm.once('negotiation-language-admin-bind').on('change', inputSelector, function (event) { + toggleTable(event.target); + }); + // Initially, hide language detection types that are not customized. + $configForm.find(inputSelector + ':not(:checked)').each(function (index, element) { + toggleTable(element); + }); + } +}; + +})(jQuery, Drupal); diff --git a/core/modules/language/language.install b/core/modules/language/language.install index b16c53b..78d3a36 100644 --- a/core/modules/language/language.install +++ b/core/modules/language/language.install @@ -22,7 +22,7 @@ function language_install() { // Enable URL language detection for each configurable language type. require_once DRUPAL_ROOT . '/core/includes/language.inc'; - foreach (language_types_get_configurable(FALSE) as $type) { + foreach (language_types_get_configurable() as $type) { language_negotiation_set($type, array(LANGUAGE_NEGOTIATION_URL => 0)); } } diff --git a/core/modules/language/language.module b/core/modules/language/language.module index 6bbb5a7..d816efb 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -560,6 +560,27 @@ function language_delete($langcode) { } /** + * Implements hook_library_info(). + */ +function language_library_info() { + $libraries['language.admin'] = array( + 'title' => 'Language detection admin', + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'language') . '/language.admin.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'jquery.once'), + ), + ); + + return $libraries; +} + + +/** * Implements hook_css_alter(). * * This function checks all CSS files currently added via drupal_add_css() and @@ -607,14 +628,17 @@ function language_language_types_info() { LANGUAGE_TYPE_INTERFACE => array( 'name' => t('User interface text'), 'description' => t('Order of language detection methods for user interface text. If a translation of user interface text is available in the detected language, it will be displayed.'), + 'locked' => TRUE, ), LANGUAGE_TYPE_CONTENT => array( 'name' => t('Content'), 'description' => t('Order of language detection methods for content. If a version of content is available in the detected language, it will be displayed.'), 'fixed' => array(LANGUAGE_NEGOTIATION_INTERFACE), + 'locked' => TRUE, ), LANGUAGE_TYPE_URL => array( 'fixed' => array(LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_URL_FALLBACK), + 'locked' => TRUE, ), ); } @@ -714,8 +738,9 @@ function language_negotiation_include() { * Implements hook_modules_enabled(). */ function language_modules_enabled($modules) { - language_negotiation_include(); - language_types_set(); + // Load configurability options from configuration. + $configurable = config('system.language.types')->get('configurable'); + language_types_set($configurable); language_negotiation_purge(); } diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc index 2182f4d..f376d4e 100644 --- a/core/modules/language/language.negotiation.inc +++ b/core/modules/language/language.negotiation.inc @@ -18,11 +18,6 @@ const LANGUAGE_NEGOTIATION_BROWSER = 'language-browser'; /** - * The language is determined using the current interface language. - */ -const LANGUAGE_NEGOTIATION_INTERFACE = 'language-interface'; - -/** * If no URL language, language is determined using an already detected one. */ const LANGUAGE_NEGOTIATION_URL_FALLBACK = 'language-url-fallback'; diff --git a/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php b/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php index d451b27..f5260f9 100644 --- a/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php +++ b/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php @@ -38,11 +38,17 @@ public function getDerivativeDefinition($derivative_id, array $base_plugin_defin public function getDerivativeDefinitions(array $base_plugin_definition) { include_once DRUPAL_ROOT . '/core/includes/language.inc'; $info = language_types_info(); - foreach (language_types_get_configurable(FALSE) as $type) { + $configurable_types = language_types_get_configurable(); + foreach ($configurable_types as $type) { $this->derivatives[$type] = $base_plugin_definition; $this->derivatives[$type]['admin_label'] = t('Language switcher (!type)', array('!type' => $info[$type]['name'])); $this->derivatives[$type]['cache'] = DRUPAL_NO_CACHE; } + // If there is just one configurable type then change the title of the + // block. + if (count($configurable_types) == 1) { + $this->derivatives[reset($configurable_types)]['admin_label'] = t('Language switcher'); + } return $this->derivatives; } diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php index 8aea763..5357139 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php @@ -68,6 +68,7 @@ function testInfoAlterations() { $form_field => TRUE, $type . '[enabled][' . $test_method_id . ']' => TRUE, $test_type . '[enabled][' . $test_method_id . ']' => TRUE, + $test_type . '[configurable]' => TRUE, ); $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings')); @@ -157,8 +158,9 @@ protected function languageNegotiationUpdate($op = 'enable') { */ protected function checkFixedLanguageTypes() { drupal_static_reset('language_types_info'); + $configurable = language_types_get_configurable(); foreach (language_types_info() as $type => $info) { - if (isset($info['fixed'])) { + if (!in_array($type, $configurable) && isset($info['fixed'])) { $negotiation = variable_get("language_negotiation_$type", array()); $equal = count($info['fixed']) == count($negotiation); while ($equal && list($id) = each($negotiation)) { diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module index cb97937..0b98ed5 100644 --- a/core/modules/language/tests/language_test/language_test.module +++ b/core/modules/language/tests/language_test/language_test.module @@ -30,6 +30,7 @@ function language_test_language_types_info() { ), 'fixed_test_language_type' => array( 'fixed' => array('test_language_negotiation_method'), + 'locked' => TRUE, ), ); } @@ -40,7 +41,7 @@ function language_test_language_types_info() { */ function language_test_language_types_info_alter(array &$language_types) { if (state()->get('language_test.content_language_type')) { - unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']); + $language_types[LANGUAGE_TYPE_CONTENT]['locked'] = FALSE; } } diff --git a/core/modules/system/config/system.language.types.yml b/core/modules/system/config/system.language.types.yml new file mode 100644 index 0000000..a0ed7ac --- /dev/null +++ b/core/modules/system/config/system.language.types.yml @@ -0,0 +1,4 @@ +configurable: + language_interface: '1' + language_content: '0' + language_url: '0' diff --git a/core/modules/system/language.api.php b/core/modules/system/language.api.php index 7c7375f..7f5a996 100644 --- a/core/modules/system/language.api.php +++ b/core/modules/system/language.api.php @@ -44,10 +44,13 @@ function hook_language_switch_links_alter(array &$links, $type, $path) { * may contain the following elements: * - name: The human-readable language type identifier. * - description: A description of the language type. + * - locked: A boolean indicating if the user can choose wether to configure + * the language type or not using the UI. * - fixed: A fixed array of language negotiation method identifiers to use to - * initialize this language. Defining this key makes the language type - * non-configurable, so it will always use the specified methods in the - * given priority order. Omit to make the language type configurable. + * initialize this language. If locked is set to TRUE and fixed is set, it + * will always use the specified methods in the given priority order. If not + * present and locked is TRUE then LANGUAGE_NEGOTIATION_INTERFACE will be + * used. * * @see hook_language_types_info_alter() * @ingroup language_negotiation @@ -57,8 +60,10 @@ function hook_language_types_info() { 'custom_language_type' => array( 'name' => t('Custom language'), 'description' => t('A custom language type.'), + 'locked' => FALSE, ), 'fixed_custom_language_type' => array( + 'locked' => TRUE, 'fixed' => array('custom_language_negotiation_method'), ), ); diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module index 7f9bf97..62a4139 100644 --- a/core/modules/translation_entity/translation_entity.module +++ b/core/modules/translation_entity/translation_entity.module @@ -63,9 +63,9 @@ function translation_entity_module_implements_alter(&$implementations, $hook) { * Implements hook_language_type_info_alter(). */ function translation_entity_language_types_info_alter(array &$language_types) { - // Make content language negotiation configurable by removing its predefined - // configuration. - unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']); + // Make content language negotiation configurable by removing unsetting the + // 'locked' flag. + $language_types[LANGUAGE_TYPE_CONTENT]['locked'] = FALSE; } /**