diff --git a/sites/all/modules/contrib/admin/date_popup_authored/.travis.yml b/sites/all/modules/contrib/admin/date_popup_authored/.travis.yml
new file mode 100644
index 00000000..fe458850
--- /dev/null
+++ b/sites/all/modules/contrib/admin/date_popup_authored/.travis.yml
@@ -0,0 +1,57 @@
+language: php
+
+php:
+ - 5.3
+ - 5.4
+ - 5.5
+ - 5.6
+ - hhvm
+
+matrix:
+ fast_finish: true
+ allow_failures:
+ - php: hhvm
+
+mysql:
+ database: date_popup_authored_test
+ username: root
+ encoding: utf8
+
+before_install:
+ - sudo apt-get update > /dev/null
+
+install:
+ # install php packages required for running a web server from drush on php 5.3
+ - sudo apt-get install -y --force-yes php5-cgi php5-mysql
+
+ # add composer's global bin directory to the path
+ # see: https://github.com/drush-ops/drush#install---composer
+ - export PATH="$HOME/.composer/vendor/bin:$PATH"
+
+ # install drush globally
+ - composer global require drush/drush:6.*
+
+before_script:
+
+ # Sendmail support.
+ - if [[ "$TRAVIS_PHP_VERSION" != hhvm* ]]; then echo 'sendmail_path = /bin/true' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi
+ # hhvm ignores sendmail_path and hhvm.mail.sendmail_path settings presently...
+ - if [[ "$TRAVIS_PHP_VERSION" == hhvm* ]]; then sudo ln -s /bin/true /usr/local/bin/sendmail; fi
+
+ # navigate out of module directory to prevent blown stack by recursive module lookup
+ - cd ../..
+
+ # create new site, stubbing sendmail path with true to prevent delivery errors and manually resolving drush path
+ - mysql -e 'create database date_popup_authored_test'
+ - php ~/.composer/vendor/bin/drush.php --yes core-quick-drupal --profile=testing --no-server --db-url=mysql://root:@127.0.0.1/date_popup_authored_test --enable=simpletest date_popup_authored_test
+
+ # reference and enable travis_ci_drupal_module_example in build site
+ - ln -s $(readlink -e $(cd -)) date_popup_authored_test/drupal/sites/all/modules/date_popup_authored
+ - cd date_popup_authored_test/drupal
+ - drush --yes pm-enable date date_popup date_popup_authored
+
+ # start a web server on port 8080, run in the background; wait for initialization
+ - drush runserver 127.0.0.1:8080 &
+ - until netstat -an 2>/dev/null | grep '8080.*LISTEN'; do true; done
+
+script: drush test-run 'Date Popup Authored' --uri=http://127.0.0.1:8080
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/CHANGELOG.markdown b/sites/all/modules/contrib/admin/date_popup_authored/CHANGELOG.markdown
new file mode 100644
index 00000000..3573910a
--- /dev/null
+++ b/sites/all/modules/contrib/admin/date_popup_authored/CHANGELOG.markdown
@@ -0,0 +1,31 @@
+# Date Popup Authored Changelog
+
+## 7.x-1.2
+
+- Travis CI support
+- [#2329773][4] by amagdy, Mark Trapp: Clean up variables when no longer in use
+- [#2051921][3] by pfrenssen: Warning: strtotime() expects parameter 1 to be string, array given in strtotime
+- [#2275593][2] by goodboy: Use date_format_short for default value.
+- [#1087616][1] by Mark Trapp: Date Popup Authored does not play nice when $form['authored']['#access'] is modified too late
+
+[1]: https://www.drupal.org/node/1087616
+[2]: https://www.drupal.org/node/2275593
+[3]: https://www.drupal.org/node/2051921
+[4]: https://www.drupal.org/node/2329773
+
+## 7.x-1.1
+
+- Drupal 7 support
+- [#1087616][6] by Mark Trapp: Post date resets on save if user can't administer nodes
+- [#1012288][5] by pillarsdotnet: Make Date Popup Authored work in PHP 5.2
+- [#995934][4] by Mark Trapp: Date Popup Authored needs tests
+- [#995060][3] by pillarsdotnet: Date Popup Authored assumes date is a DateObject, shouldn't
+- [#970622][2] by Mark Trapp: Saving a node with Date Popup Authored enabled will result in published time drift
+- [#970406][1] by Mark Trapp: Creating a new node with Date Popup Authored enabled results in a White Screen of Death
+
+[1]: https://www.drupal.org/node/970406
+[2]: https://www.drupal.org/node/970622
+[3]: https://www.drupal.org/node/995060
+[4]: https://www.drupal.org/node/995934
+[5]: https://www.drupal.org/node/1012288
+[6]: https://www.drupal.org/node/1087616
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/CHANGELOG.txt b/sites/all/modules/contrib/admin/date_popup_authored/CHANGELOG.txt
deleted file mode 100644
index 7d777e14..00000000
--- a/sites/all/modules/contrib/admin/date_popup_authored/CHANGELOG.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-Date Popup Authored Module 7.x
-
-Date Popup Authored 7.x-1.1
----------------------------
-Stable release to coincide with stable release of Date.
-
-Date Popup Authored 7.x-1.1-beta2
--------------------------------
-#1087616 by Mark Trapp: Post date resets on save if user can't administer nodes
-
-Date Popup Authored 7.x-1.1-beta1
--------------------------------
-#1012288 by pillarsdotnet: Make Date Popup Authored work in PHP 5.2
-
-Date Popup Authored 7.x-1.1-alpha4
--------------------------------
-#995934 by Mark Trapp: Date Popup Authored needs tests
-#995060 by pillarsdotnet: Date Popup Authored assumes date is a DateObject, shouldn't
-
-Date Popup Authored 7.x-1.1-alpha3
-----------------------------------
-#970622 by Mark Trapp: Saving a node with Date Popup Authored enabled will result in published time drift
-
-Date Popup Authored 7.x-1.1-alpha2
-----------------------------------
-#970406 by Mark Trapp: Creating a new node with Date Popup Authored enabled results in a White Screen of Death
-
-Date Popup Authored 7.x-1.1-alpha1
-----------------------------------
-Initial unstable release of the Drupal 7 port.
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/README.markdown b/sites/all/modules/contrib/admin/date_popup_authored/README.markdown
new file mode 100644
index 00000000..badd0e78
--- /dev/null
+++ b/sites/all/modules/contrib/admin/date_popup_authored/README.markdown
@@ -0,0 +1,78 @@
+# Date Popup Authored
+
+[](https://travis-ci.org/itafroma/drupal-date_popup_authored)
+
+## Introduction
+
+Date Popup Authored provides a jQuery UI datepicker for the "Authored on" date field found on node submission forms.
+
+For a full description of the module, visit the [project page][1] on Drupal.org.
+To submit bug reports and feature suggestions, or to track changes, please visit the [issue queue][2].
+
+[1]: https://drupal.org/project/date_popup_authored "Date Popup Authored project page"
+[2]: https://drupal.org/project/issues/date_popup_authored "Date Popup Authored issue tracker"
+
+## Requirements
+
+- Drupal 7
+- [Date][3] 2.0 or later
+- Date Popup, part of the Date module
+
+[3]: https://drupal.org/project/date "Date project page"
+
+## Installation and configuration
+
+Install as usual. See the [handbook page on installing contributed modules][4] for further information.
+
+You can change the behavior of the date-picker by going to the settings page for each content type.
+
+[4]: https://drupal.org/node/895232 "Installing modules (Drupal 7)"
+
+## Caveats
+
+Since Date Popup Authored allows you to choose a date format that's less specific than the default date format Drupal uses for the Authored on field, it will insert default data if you use a more simplified date format.
+
+For example, if the date format you've configured doesn't include a time, when the node is saved, the Authored on time will be set to 12:00AM. Similarly, if you don't include the ability to choose a month, the Authored on month will be set to January (i.e. month 1).
+
+So, if you care about the time a post is authored, make sure you allow the user to set it in the date format. See installation for more information.
+
+## Future development
+
+The functionality this module provides is being considered for core inclusion:
+
+- [#1835016: Polyfill date input type][1]
+- [#471942-30: Use Date Popup on 'Authored on' field][2]
+- [#504524: Extend Authored on field with jQuery UI Date Picker][3]
+
+Because of this, there will hopefully be no Drupal 8 version.
+
+[5]: https://www.drupal.org/node/1835016 "#1835016: Polyfill date input type"
+[6]: https://www.drupal.org/comment/6788664#comment-6788664 "#471942-30: Use Date Popup on 'Authored on' field"
+[7]: https://www.drupal.org/node/504524 "#504524: Extend Authored on field with jQuery UI Date Picker"
+
+## Contact
+
+The current maintainer is [Mark Trapp][5] ([Drupal.org profile][6]).
+
+[8]: http://marktrapp.com "Mark Trapp's website"
+[9]: https://drupal.org/u/mark-trapp "Mark Trapp's Drupal.org profile"
+
+## Acknowledgments
+
+Date Popup Authored was inspired by the hacks provided by [brice][7] and [Rob Loach][8] in the Date module issue, "[Use Date Popup on 'Authored on' field][9]." It contains additional fixes to account for problems found in their solution, new configuration options, Drupal 7 support, and a full test suite.
+
+[10]: https://drupal.org/user/446296 "brice's Drupal.org profile"
+[11]: https://drupal.org/u/robloach "Rob Loach's Drupal.org profile"
+[12]: https://drupal.org/node/471942 "Use Date Popup on 'Authored on' field"
+
+## More information
+
+- For additional documentation, see the [online Drupal handbook][10].
+- For a list of security announcements, see the [*Security advisories* page][11] (available as an RSS feed). This page also describes how to subscribe to these announcements via e-mail.
+- For information about the Drupal security process, or to find out how to report a potential security issue to the Drupal security team, see the [*Security team* page][12].
+- For information about the wide range of available support options, see the [*Support* page][13].
+
+[13]: https://drupal.org/handbook "Drupal Handbook"
+[14]: https://drupal.org/security "Drupal security advisories"
+[15]: https://drupal.org/security-team "Drupal security team"
+[16]: https://drupal.org/support] "Drupal support"
\ No newline at end of file
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/README.txt b/sites/all/modules/contrib/admin/date_popup_authored/README.txt
deleted file mode 100644
index 320bf89b..00000000
--- a/sites/all/modules/contrib/admin/date_popup_authored/README.txt
+++ /dev/null
@@ -1,76 +0,0 @@
-
-CONTENTS OF THIS FILE
----------------------
-
-* Introduction
-* Requirements
-* Installation
-* Acknowledgements
-* Contact
-* More Information
-
-INTRODUCTION
-------------
-
-Date Popup Authored provides a jQuery UI datepicker for the "Authored on"
-date field found on node submission forms.
-
-For a full description of the module, visit the project page:
- http://drupal.org/project/date_popup_authored
-
-To submit bug reports and feature suggestions, or to track changes:
- http://drupal.org/project/issues/date_popup_authored
-
-REQUIREMENTS
-------------
-
-- Drupal 7
-- Date [1] 7.x-2.0 or later
-- Date Popup, part of the Date module
-
-[1] http://drupal.org/project/date
-
-INSTALLATION
-------------
-
-Install as usual. See the handbook page on installing contributed modules [1]
-for further information.
-
-You can change the behavior of the datepicker by going to the settings page
-for each content type.
-
-[1] http://drupal.org/node/895232
-
-CONTACT
--------
-
-Current maintainer:
-- Mark Trapp (amorfati) - http://drupal.org/user/212019
-
-ACKNOWLEDGEMENTS
-----------------
-
-Date Popup Authored was inspired by the hacks provided by brice [1] and
-Rob Loach [2] in issue #471942 [3]. It contains additional fixes to account for
-problems found in their solution as well as new configuration options.
-
-[1] http://drupal.org/user/446296
-[2] http://drupal.org/user/61114
-[3] http://drupal.org/node/471942
-
-MORE INFORMATION
-----------------
-
-- For additional documentation, see the online Drupal handbook at
- http://drupal.org/handbook.
-
-- For a list of security announcements, see the "Security announcements" page
- at http://drupal.org/security (available as an RSS feed). This page also
- describes how to subscribe to these announcements via e-mail.
-
-- For information about the Drupal security process, or to find out how to report
- a potential security issue to the Drupal security team, see the "Security team"
- page at http://drupal.org/security-team.
-
-- For information about the wide range of available support options, see the
- "Support" page at http://drupal.org/support.
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.info b/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.info
index bb7cfc74..6f8bb86f 100644
--- a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.info
+++ b/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.info
@@ -1,12 +1,13 @@
name = "Date Popup Authored"
description = "Provides a datepicker for the 'Authored on' field on node forms."
core = 7.x
-files[] = date_popup_authored.test
+files[] = tests/date_popup_authored_format.test
package = Date/Time
dependencies[] = date_popup
-; Information added by drupal.org packaging script on 2012-03-31
-version = "7.x-1.1"
+
+; Information added by Drupal.org packaging script on 2014-12-22
+version = "7.x-1.2"
core = "7.x"
project = "date_popup_authored"
-datestamp = "1333178146"
+datestamp = "1419241381"
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.install b/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.install
new file mode 100644
index 00000000..8e4095e4
--- /dev/null
+++ b/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.install
@@ -0,0 +1,17 @@
+type);
+ variable_del('date_popup_authored_format_' . $node_type->type);
+ variable_del('date_popup_authored_year_range_' . $node_type->type);
+ }
+}
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.module b/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.module
index a761adc6..9bf1038b 100644
--- a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.module
+++ b/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.module
@@ -110,39 +110,80 @@ function date_popup_authored_form_node_type_form_alter(&$form, &$form_state, $fo
}
/**
- * Implements hook_form_alter().
+ * Implements hook_form_BASE_FORM_ID_alter() for node_form.
+ *
+ * Replaces default Authored on field with a datepicker on node submission forms.
*/
-function date_popup_authored_form_alter(&$form, $form_state, $form_id) {
+function date_popup_authored_form_node_form_alter(&$form, $form_state, $form_id) {
+ // Date Popup Authored should modify the node submission form only if the
+ // user is allowed to modify the authoring information and it's enabled.
+ if (!($form['author']['#access'] && variable_get('date_popup_authored_enabled_' . $form['type']['#value'], 1))) {
+ return;
+ }
- // Replaces default Authored on field with a datepicker on node submission forms.
- if (strstr($form_id, '_node_form') !== FALSE) {
- // Date Popup Authored should modify the node submission form only if the
- // user is allowed to modify the authoring information and it's enabled.
- if (!($form['author']['#access'] && variable_get('date_popup_authored_enabled_' . $form['type']['#value'], 1))) {
- return;
- }
+ $form['author']['date']['#type'] = 'date_popup';
- $form['author']['date']['#type'] = 'date_popup';
+ // If there is a date already available, rewrite it to conform to
+ // Date Popup's expected format.
+ if (!empty($form['author']['date']['#default_value'])) {
+ $date = new DateObject($form['author']['date']['#default_value'], NULL, 'Y-m-d H:i:s O');
+ $form['author']['date']['#default_value'] = $date->format('Y-m-d H:i:s');
+ }
- // If there is a date already available, rewrite it to conform to
- // Date Popup's expected format.
- if (!empty($form['author']['date']['#default_value'])) {
- $date = new DateObject($form['author']['date']['#default_value'], NULL, 'Y-m-d H:i:s O');
- $form['author']['date']['#default_value'] = $date->format('Y-m-d H:i:s');
- }
+ // Set options specific to date_popup.
+ $year_range = variable_get('date_popup_authored_year_range_' . $form['type']['#value'], 3);
+ $form['author']['date']['#date_year_range'] = '-' . $year_range . ':+' . $year_range;
+ $form['author']['date']['#date_format'] = variable_get('date_popup_authored_format_' . $form['type']['#value'], variable_get('date_format_short', 'm/d/Y - H:i'));
- // Set options specific to date_popup.
- $year_range = variable_get('date_popup_authored_year_range_' . $form['type']['#value'], 3);
- $form['author']['date']['#date_year_range'] = '-' . $year_range . ':+' . $year_range;
- $form['author']['date']['#date_format'] = variable_get('date_popup_authored_format_' . $form['type']['#value'], 'm/d/Y - H:i');
+ // Unset options that are not relevant to date_popup
+ unset($form['author']['date']['#maxlength']);
+ unset($form['author']['date']['#description']);
- // Unset options that are not relevant to date_popup
- unset($form['author']['date']['#maxlength']);
- unset($form['author']['date']['#description']);
+ // Add an additional validate handler after date_popup builds the element.
+ $form['author']['date']['#after_build'][] = 'date_popup_authored_element_after_build';
- // We need to modify date popup's data during submit
- // @see http://drupal.org/node/847854
- $form['#submit'][] = 'date_popup_authored_node_form_submit';
+ // We need to modify date popup's data during submit
+ // @see http://drupal.org/node/847854
+ $form['#submit'][] = 'date_popup_authored_node_form_submit';
+}
+
+/**
+ * Implements hook_node_type_delete().
+ */
+function date_popup_authored_node_type_delete($info) {
+ // Delete format configuration when node types are deleted.
+ variable_del('date_popup_authored_enabled_' . $info->type);
+ variable_del('date_popup_authored_format_' . $info->type);
+ variable_del('date_popup_authored_year_range_' . $info->type);
+}
+
+/**
+ * Form after build handler for the date popup element.
+ */
+function date_popup_authored_element_after_build($element, &$form_state) {
+ // Add a validate handler after the one that is added by date_popup.
+ $element['#element_validate'][] = 'date_popup_authored_element_validate';
+ return $element;
+}
+
+/**
+ * Validate handler for the date popup element.
+ */
+function date_popup_authored_element_validate($element, &$form_state) {
+ if (date_hidden_element($element) || is_string($element['#value'])) {
+ return;
+ }
+
+ // If an error occurred in the validation of the date popup field the date
+ // cannot be correctly rendered as a string. In this case clear the date value
+ // to avoid subsequent errors when the node is validated.
+ // @see date_popup_validate()
+ // @see node_validate()
+ $input_exists = NULL;
+ $input = drupal_array_get_nested_value($form_state['values'], $element['#parents'], $input_exists);
+ $date = date_popup_input_date($element, $input);
+ if (is_object($date) && !empty($date->errors)) {
+ $form_state['values']['date'] = NULL;
}
}
diff --git a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.test b/sites/all/modules/contrib/admin/date_popup_authored/tests/date_popup_authored_format.test
similarity index 60%
rename from sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.test
rename to sites/all/modules/contrib/admin/date_popup_authored/tests/date_popup_authored_format.test
index d735f331..28958e49 100644
--- a/sites/all/modules/contrib/admin/date_popup_authored/date_popup_authored.test
+++ b/sites/all/modules/contrib/admin/date_popup_authored/tests/date_popup_authored_format.test
@@ -87,7 +87,7 @@ class DatePopupAuthoredTestCase extends PageEditTestCase {
debug($node_date->format($time_format), 'Node time');
// Extract the date and time parts as seen on the node submission form
- $default_format = variable_get('date_popup_authored_format_page', 'm/d/Y - H:i');
+ $default_format = variable_get('date_popup_authored_format_page', variable_get('date_format_short', 'm/d/Y - H:i'));
variable_set('date_popup_authored_format_page', $format);
$this->drupalGet('node/' . $node->nid . '/edit');
@@ -108,4 +108,80 @@ class DatePopupAuthoredTestCase extends PageEditTestCase {
// Reset format back to default
variable_set('date_popup_authored_format_page', $format);
}
+
+ /**
+ * Tests form field validation.
+ */
+ function testFieldValidation() {
+ // Define some test cases.
+ $test_cases = array(
+ array(
+ 'description' => 'a valid date field and a missing time field',
+ 'date' => '02/07/2014',
+ 'time' => '',
+ 'valid' => FALSE,
+ ),
+ array(
+ 'description' => 'a valid date field and a valid time field',
+ 'date' => '02/07/2014',
+ 'time' => '12:00',
+ 'valid' => TRUE,
+ ),
+ );
+
+ // Log in as administrator.
+ $this->drupalLogin($this->admin_user);
+
+ // Test the test cases.
+ foreach ($test_cases as $test_case) {
+ $edit = array(
+ 'title' => $this->randomString(),
+ 'date[date]' => $test_case['date'],
+ 'date[time]' => $test_case['time'],
+ );
+ $this->drupalPost('node/add/page', $edit, t('Save'));
+
+ $error_messages = $this->xpath('//div[contains(@class, "error")]');
+
+ $message = format_string('When submitting a node with @description the form validation correctly @result.', array(
+ '@description' => $test_case['description'],
+ '@result' => $test_case['valid'] ? 'succeeds' : 'fails',
+ ));
+ $this->assertEqual(empty($error_messages), $test_case['valid'], $message);
+ }
+ }
+
+ /**
+ * Tests variable cleanup after a content type is removed.
+ */
+ function testVariableCleanupAfterNodeTypeRemoval() {
+ $node_type_name = strtolower($this->randomName(8) . '_test');
+ $node_type = $this->drupalCreateContentType(array('name' => $node_type_name, 'type' => $node_type_name));
+
+ variable_set('date_popup_authored_enabled_' . $node_type_name, 'foo');
+ variable_set('date_popup_authored_format_' . $node_type_name, 'foo');
+ variable_set('date_popup_authored_year_range_' . $node_type_name, 'foo');
+
+ node_type_delete($node_type_name);
+
+ $this->assertNull(variable_get('date_popup_authored_enabled_' . $node_type_name));
+ $this->assertNull(variable_get('date_popup_authored_format_' . $node_type_name));
+ $this->assertNull(variable_get('date_popup_authored_year_range_' . $node_type_name));
+ }
+
+ /**
+ * Tests variable cleanup after Date Popup Authored is uninstalled.
+ */
+ function testVariableCleanupAfterUninstall() {
+ variable_set('date_popup_authored_enabled_page', 'foo');
+ variable_set('date_popup_authored_format_page', 'foo');
+ variable_set('date_popup_authored_year_range_page', 'foo');
+
+ module_disable(array('date_popup_authored'));
+ drupal_uninstall_modules(array('date_popup_authored'));
+
+ $this->assertNull(variable_get('date_popup_authored_enabled_page'));
+ $this->assertNull(variable_get('date_popup_authored_format_page'));
+ $this->assertNull(variable_get('date_popup_authored_year_range_page'));
+ }
}
diff --git a/sites/all/modules/contrib/admin/features/features.admin.inc b/sites/all/modules/contrib/admin/features/features.admin.inc
index 4e12576f..a4203a02 100644
--- a/sites/all/modules/contrib/admin/features/features.admin.inc
+++ b/sites/all/modules/contrib/admin/features/features.admin.inc
@@ -19,16 +19,39 @@ function features_settings_form($form, $form_state) {
'#title' => t('Show components on create/edit feature form.'),
'#description' => t('Components with no options will not be shown no matter the setting below. Disabled components cannot be used with admin form.')
);
- foreach ($components as $compontent => $info) {
+
+ $form['lock_components'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Lock components'),
+ '#description' => t('Locked components will be prevented from ever being reverted. For example, if site builder updates a feature with new settings for a field instance, but field instance is locked, it will not update that field. If the item is purely in code, like a view, the view changed when the code is updated no matter these settings.')
+ );
+ $form['features_lock_mode'] = array(
+ '#type' => 'radios',
+ '#title' => t('Features lock mode'),
+ '#options' => array(
+ 'rebuild' => t('Allow rebuild (prevent revert)'),
+ 'all' => t('Prevent rebuild and revert'),
+ ),
+ '#description' => t('Rebuild will allow the feature to be updated till the point features has detected that the item has changed deliberately on the site, e.g. is overriden.'),
+ '#default_value' => variable_get('features_lock_mode', 'all'),
+ );
+ foreach ($components as $component => $info) {
if (empty($info['feature_source']) && empty($info['features_source'])) {
continue;
}
- $form['show_components']['features_admin_show_component_' . $compontent] = array(
- '#title' => t('@name (@machine)', array('@name' => $info['name'], '@machine' => $compontent)),
+ $form['show_components']['features_admin_show_component_' . $component] = array(
+ '#title' => t('@name (@machine)', array('@name' => $info['name'], '@machine' => $component)),
'#type' => 'checkbox',
- '#default_value' => variable_get('features_admin_show_component_' . $compontent, TRUE),
+ '#default_value' => variable_get('features_admin_show_component_' . $component, TRUE),
);
- if ($compontent == 'menu_links' && ($menus = menu_get_menus())) {
+ if (features_hook($component, 'features_revert') || features_hook($component, 'features_rebuild')) {
+ $form['lock_components']['features_component_locked_' . $component] = array(
+ '#title' => t('@name (@machine)', array('@name' => $info['name'], '@machine' => $component)),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('features_component_locked_' . $component, FALSE),
+ );
+ }
+ if ($component == 'menu_links' && ($menus = menu_get_menus())) {
$form['show_components']['features_admin_menu_links'] = array(
'#title' => t('Advanced Menu Link Settings'),
'#type' => 'fieldset',
@@ -104,23 +127,24 @@ function features_export_form($form, $form_state, $feature = NULL) {
'#description' => t('Example: Image gallery') . ' (' . t('Do not begin name with numbers.') . ')',
'#type' => 'textfield',
'#default_value' => !empty($feature->info['name']) ? $feature->info['name'] : '',
- '#attributes' => array('class' => array('feature-name')),
);
$form['info']['module_name'] = array(
- '#type' => 'textfield',
+ '#type' => 'machine_name',
'#title' => t('Machine-readable name'),
'#description' => t('Example: image_gallery') . '
' . t('May only contain lowercase letters, numbers and underscores. Try to avoid conflicts with the names of existing Drupal projects.'),
'#required' => TRUE,
'#default_value' => $feature_name,
- '#attributes' => array('class' => array('feature-module-name')),
- '#element_validate' => array('features_export_form_validate_field'),
+ '#machine_name' => array(
+ 'exists' => 'features_export_form_module_name_exists',
+ 'source' => array('info', 'name'),
+ ),
);
- // If recreating this feature, disable machine name field and blank out
- // js-attachment classes to ensure the machine name cannot be changed.
- if (isset($feature)) {
+ // If recreating this feature, disable machine name field to ensure the
+ // machine name cannot be changed, unless user role has granted permission to
+ // edit machine name of disabled features.
+ if (isset($feature) && ($feature->status || !user_access('rename features'))) {
$form['info']['module_name']['#value'] = $feature_name;
$form['info']['module_name']['#disabled'] = TRUE;
- $form['info']['name']['#attributes'] = array();
}
$form['info']['description'] = array(
'#title' => t('Description'),
@@ -223,6 +247,13 @@ function features_export_form($form, $form_state, $feature = NULL) {
return $form;
}
+/**
+ * Machine name existence callback for the module name.
+ */
+function features_export_form_module_name_exists($value) {
+ return (bool) features_get_info('module', $value);
+}
+
/**
* Return the render array elements for the Components selection on the Export form
* @param array $feature - feature associative array
@@ -779,16 +810,6 @@ function features_info_file_preview($form, &$form_state){
*/
function features_export_form_validate_field($element, &$form_state) {
switch ($element['#name']) {
- case 'module_name':
- if (!preg_match('!^[a-z0-9_]+$!', $element['#value'])) {
- form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
- }
- // If user is filling out the feature name for the first time and uses
- // the name of an existing module throw an error.
- else if (empty($element['#default_value']) && features_get_info('module', $element['#value'])) {
- form_error($element, t('A module by the name @name already exists on your site. Please choose a different name.', array('@name' => $element['#value'])));
- }
- break;
case 'project_status_url':
if (!empty($element['#value']) && !valid_url($element['#value'])) {
form_error($element, t('The URL %url is invalid. Please enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $element['#value'])));
@@ -988,7 +1009,7 @@ function features_admin_form($form, $form_state) {
ksort($features);
foreach ($features as $name => $module) {
$package_title = !empty($module->info['package']) ? $module->info['package'] : t('Other');
- $package = strtolower(preg_replace('/[^a-zA-Z0-9-]+/', '-', $package_title));
+ $package = 'package_' . strtolower(preg_replace('/[^a-zA-Z0-9-]+/', '-', $package_title));
// Set up package elements
if (!isset($form[$package])) {
@@ -1052,6 +1073,7 @@ function features_admin_form($form, $form_state) {
}
$href = "admin/structure/features/{$name}";
+ $href_overridden = module_exists('diff') ? $href . '/diff' : $href;
$module_name = (user_access('administer features')) ? l($module->info['name'], $href) : $module->info['name'];
$form[$package]['status'][$name] = array(
'#type' => 'checkbox',
@@ -1081,7 +1103,7 @@ function features_admin_form($form, $form_state) {
$state .= l(t('Check'), "admin/structure/features/{$name}/status", array('attributes' => array('class' => array('admin-check'))));
$state .= theme('features_storage_link', array('storage' => FEATURES_REBUILDING, 'path' => $href));
$state .= theme('features_storage_link', array('storage' => FEATURES_NEEDS_REVIEW, 'path' => $href));
- $state .= theme('features_storage_link', array('storage' => FEATURES_OVERRIDDEN, 'path' => $href));
+ $state .= theme('features_storage_link', array('storage' => FEATURES_OVERRIDDEN, 'path' => $href_overridden));
$state .= theme('features_storage_link', array('storage' => FEATURES_DEFAULT, 'path' => $href));
}
elseif (!empty($conflicts[$name])) {
@@ -1131,7 +1153,7 @@ function features_admin_components($form, $form_state, $feature) {
drupal_set_breadcrumb($breadcrumb);
module_load_include('inc', 'features', 'features.export');
- $form = array();
+ $form['#feature'] = $feature;
// Store feature info for theme layer.
$form['module'] = array('#type' => 'value', '#value' => $feature->name);
@@ -1195,8 +1217,14 @@ function features_admin_components($form, $form_state, $feature) {
else if (array_key_exists($component, $conflicts)) {
$storage = FEATURES_CONFLICT;
}
+ // This can be removed if the css is fixed so link doesn't move when
+ // ajaxing and linke moved.
+ $lock_link = '';
+ if (user_access('administer features') && (features_hook($component, 'features_revert') || features_hook($component, 'features_rebuild'))) {
+ $lock_link = ' ' . theme('features_lock_link', array('feature' => $feature->name, 'component' => $component));
+ }
$form['components'][$component] = array(
- '#markup' => theme('features_storage_link', array('storage' => $storage, 'path' => $path)),
+ '#markup' => $lock_link . theme('features_storage_link', array('storage' => $storage, 'path' => $path)),
);
}
@@ -1367,6 +1395,71 @@ function features_feature_diff($feature, $component = NULL) {
return $output;
}
+
+
+/**
+ * Page callback to lock a component.
+ *
+ * @param $feature
+ * Loaded feature object to be processed for component locking.
+ * @param $component
+ * (optional) A specific component to lock.
+ *
+ * @return
+ * Themed display of what is different.
+ */
+function features_admin_lock($feature, $type = 'ajax', $component = NULL) {
+ if ($type == 'ajax' && !empty($_GET['token']) && drupal_valid_token($_GET['token'], 'features/' . $feature->name . '/' . ($component ? $component : '')) == $_GET['token']) {
+ if (features_feature_is_locked($feature->name, $component, FALSE)) {
+ features_feature_unlock($feature->name, $component);
+ }
+ else {
+ features_feature_lock($feature->name, $component);
+ }
+ $commands = array();
+ $new_link = theme('features_lock_link', array('feature' => $feature->name, 'component' => $component));
+ $commands[] = ajax_command_replace('#features-lock-link-' . $feature->name . ($component ? '-' . $component : ''), $new_link);
+ $page = array('#type' => 'ajax', '#commands' => $commands);
+ ajax_deliver($page);
+ }
+ else {
+ return drupal_get_form('features_feature_lock_confirm_form', $feature, $component);
+ }
+}
+
+/**
+ * Confirm form for locking a feature.
+ */
+function features_feature_lock_confirm_form($form, $form_state, $feature, $component) {
+ $form['#feature'] = $feature;
+ $form['#component'] = $component;
+ $is_locked = features_feature_is_locked($feature->name, $component, FALSE);
+ $args = array(
+ '@name' => $feature->name,
+ '@component' => $component ? $component : t('all'),
+ '!action' => $is_locked ? t('unlock') : t('lock'),
+ );
+ $question = t('Are you sure you want to !action this Feature @name (component @component)?', $args);
+ return confirm_form($form, $question, 'admin/structure/features/' . $feature->name);
+}
+
+/**
+ * Submit callback to lock components of a feature.
+ */
+function features_feature_lock_confirm_form_submit($form, &$form_state) {
+ $feature = $form['#feature']->name;
+ $component = $form['#component'];
+ if (features_feature_is_locked($feature, $component, FALSE)) {
+ features_feature_unlock($feature, $component);
+ drupal_set_message(t('Feature @name (component @component) has been unlocked.', array('@name' => $feature, '@component' => $component ? $component : t('all'))));
+ }
+ else {
+ features_feature_lock($feature, $component);
+ drupal_set_message(t('Feature @name (component @component) has been locked.', array('@name' => $feature, '@component' => $component ? $component : t('all'))));
+ }
+ $form_state['redirect'] = 'admin/structure/features/' . $feature;
+}
+
/**
* Compare the component names. Used to sort alphabetically.
*/
diff --git a/sites/all/modules/contrib/admin/features/features.api.php b/sites/all/modules/contrib/admin/features/features.api.php
index 10915e16..f5984e0c 100644
--- a/sites/all/modules/contrib/admin/features/features.api.php
+++ b/sites/all/modules/contrib/admin/features/features.api.php
@@ -219,6 +219,42 @@ function hook_features_rebuild($module_name) {
}
}
+/**
+ * Invoked before a restore operation is run.
+ *
+ * This hook is called before any of the restore operations on the components is
+ * run.
+ *
+ * @param string $op
+ * The operation that is triggered: revert, rebuild, disable, enable
+ * @param array $items
+ * The items handled by the operation.
+ */
+function hook_features_pre_restore($op, $items) {
+ if ($op == 'rebuild') {
+ // Use features rebuild to rebuild the features independent exports too.
+ entity_defaults_rebuild();
+ }
+}
+
+/**
+ * Invoked after a restore operation is run.
+ *
+ * This hook is called after any of the restore operations on the components is
+ * run.
+ *
+ * @param string $op
+ * The operation that is triggered: revert, rebuild, disable, enable
+ * @param array $items
+ * The items handled by the operation.
+ */
+function hook_features_post_restore($op, $items) {
+ if ($op == 'rebuild') {
+ // Use features rebuild to rebuild the features independent exports too.
+ entity_defaults_rebuild();
+ }
+}
+
/**
* Alter the final array of Component names to be exported, just prior to
* the rendering of defaults. Allows modules a final say in whether or not
@@ -293,6 +329,8 @@ function hook_features_pipe_alter(&$pipe, $data, $export) {
*/
/**
+ * Deprecated as of 7.x-2.0.
+ *
* Alter the default fields right before they are cached into the database.
*
* @param &$fields
@@ -301,6 +339,24 @@ function hook_features_pipe_alter(&$pipe, $data, $export) {
function hook_field_default_fields_alter(&$fields) {
}
+/**
+ * Alter the base fields right before they are cached into the database.
+ *
+ * @param &$fields
+ * By reference. The fields that have been declared by another feature.
+ */
+function hook_field_default_field_bases_alter(&$fields) {
+}
+
+/**
+ * Alter the field instances right before they are cached into the database.
+ *
+ * @param &$fields
+ * By reference. The fields that have been declared by another feature.
+ */
+function hook_field_default_field_instances_alter(&$fields) {
+}
+
/**
* Alter the default fieldgroup groups right before they are cached into the
* database.
diff --git a/sites/all/modules/contrib/admin/features/features.css b/sites/all/modules/contrib/admin/features/features.css
index 16c7f723..e3f0251d 100644
--- a/sites/all/modules/contrib/admin/features/features.css
+++ b/sites/all/modules/contrib/admin/features/features.css
@@ -563,4 +563,17 @@ input.form-submit.features-refresh-button {
fieldset.features-export-component .fieldset-title .component-count {
font-size: 12px;
font-weight: bold;
+}
+
+.features-lock-icon {
+ display: inline-block;
+}
+
+.features-components h3 {
+ display: inline-block;
+}
+
+.features-lock-empty {
+ display: inline-block;
+ width: 16px;
}
\ No newline at end of file
diff --git a/sites/all/modules/contrib/admin/features/features.drush.inc b/sites/all/modules/contrib/admin/features/features.drush.inc
index 9da547dd..d9f3a2ec 100644
--- a/sites/all/modules/contrib/admin/features/features.drush.inc
+++ b/sites/all/modules/contrib/admin/features/features.drush.inc
@@ -43,6 +43,7 @@ function features_drush_command() {
'destination' => "Destination path (from Drupal root) of the exported feature. Defaults to '" . $path . "'.",
'version-set' => "Specify a version number for the feature.",
'version-increment' => "Increment the feature's version number.",
+ 'ignore-conflicts' => "Ignore conflicts and export all components.",
),
'drupal dependencies' => array('features'),
'aliases' => array('fe'),
@@ -72,6 +73,9 @@ function features_drush_command() {
'not-exported' => array(
'description' => 'Show only components that have not been exported.',
),
+ 'info-style' => array(
+ 'description' => 'Export components in format suitable for using in an info file.',
+ ),
),
'aliases' => array('fc'),
);
@@ -134,6 +138,18 @@ function features_drush_command() {
'aliases' => array('fd'),
);
+ $items['features-diff-all'] = array(
+ 'description' => "Show the code difference for all enabled features not in their default state.",
+ 'arguments' => array(
+ 'feature_exclude' => 'A space-delimited list of features to exclude from being reverted.',
+ ),
+ 'options' => array(
+ 'force' => "Bypass the confirmations. This is useful if you want to output all of the diffs to a log file.",
+ ),
+ 'drupal dependencies' => array('features', 'diff'),
+ 'aliases' => array('fda'),
+ );
+
return $items;
}
@@ -232,6 +248,7 @@ function drush_features_list() {
function drush_features_components() {
$args = func_get_args();
$components = _drush_features_component_list();
+ ksort($components);
// If no args supplied, prompt with a list.
if (empty($args)) {
$types = array_keys($components);
@@ -252,10 +269,13 @@ function drush_features_components() {
elseif (drush_get_option(array('not-exported', 'o'), NULL)) {
$options['exported'] = FALSE;
}
+ if (drush_get_option(array('info-style', 'is'), NULL)) {
+ $options['info style'] = TRUE;
+ }
$filtered_components = _drush_features_component_filter($components, $args, $options);
if ($filtered_components){
- _drush_features_component_print($filtered_components);
+ _drush_features_component_print($filtered_components, $options);
}
}
@@ -289,7 +309,7 @@ function _drush_features_component_filter($all_components, $patterns = array(),
// First filter on exported state.
foreach ($all_components as $source => $components) {
foreach ($components as $name => $title) {
- $exported = sizeof($components_map[$source][$name]) > 0;
+ $exported = !empty($components_map[$source][$name]);
if ($exported) {
if ($options['exported']) {
$pool[$source][$name] = $title;
@@ -317,7 +337,8 @@ function _drush_features_component_filter($all_components, $patterns = array(),
// Rewrite * to %. Let users use both as wildcard.
$pattern = strtr($pattern, array('*' => '%'));
$sources = array();
- list($source_pattern, $component_pattern) = explode(':', $pattern, 2);
+ $source_pattern = strtok($pattern, ':');
+ $component_pattern = strtok(':');
// If source is empty, use a pattern.
if ($source_pattern == '') {
$source_pattern = '%';
@@ -382,7 +403,7 @@ function _drush_features_component_filter($all_components, $patterns = array(),
return drush_set_error('', dt('Ambiguous component "!component", matches !matches', array('!component' => $component_pattern, '!matches' => join(', ', $matches))));
}
}
- if (!is_array($selected[$source])) {
+ if (empty($selected[$source])) {
$selected[$source] = array();
}
$selected[$source] += array_intersect_key($pool[$source], array_flip($matches));
@@ -403,7 +424,7 @@ function _drush_features_component_filter($all_components, $patterns = array(),
if ($options['provided by'] && $options['exported'] ) {
foreach ($selected as $source => $components) {
foreach ($components as $name => $title) {
- $exported = sizeof($components_map[$source][$name]) > 0;
+ $exported = !empty($components_map[$source][$name]);
if ($exported) {
$provided_by[$source . ':' . $name] = join(', ', $components_map[$source][$name]);
}
@@ -420,11 +441,17 @@ function _drush_features_component_filter($all_components, $patterns = array(),
/**
* Prints a list of filtered components.
*/
-function _drush_features_component_print($filtered_components) {
+function _drush_features_component_print($filtered_components, $options = array()) {
$rows = array(array(dt('Available sources')));
foreach ($filtered_components['components'] as $source => $components) {
foreach ($components as $name => $value) {
- $row = array($source .':'. $name);
+ if (!empty($options['info style'])) {
+ // Output as .info file style.
+ $row = array('features[' . $source . '][] = "' . $name . '"');
+ }
+ else {
+ $row = array($source .':'. $name);
+ }
if (isset($filtered_components['sources'][$source .':'. $name])) {
$row[] = dt('Provided by') . ': ' . $filtered_components['sources'][$source .':'. $name];
}
@@ -446,9 +473,11 @@ function drush_features_export() {
return drush_set_error('', 'No components supplied.');
}
$components = _drush_features_component_list();
- $options = array(
- 'exported' => FALSE,
- );
+ $options = array();
+
+ if (!drush_get_option('ignore-conflicts', FALSE)) {
+ $options['exported'] = FALSE;
+ }
$filtered_components = _drush_features_component_filter($components, $args, $options);
$items = $filtered_components['components'];
@@ -568,11 +597,9 @@ function _drush_features_export($info, $module_name = NULL, $directory = NULL) {
$destination = drush_get_option(array('destination'), variable_get('features_default_export_path', FEATURES_DEFAULT_EXPORT_PATH));
$directory = isset($directory) ? $directory : $destination . '/' . $module_name;
if (is_dir($directory)) {
- $warning = dt('Module appears to already exist in !dir', array('!dir' => $directory));
- drush_log($warning, 'warning');
// If we aren't skipping confirmation and the directory already exists,
- // prompt the user.
- if (!$skip_confirmation && !drush_confirm(dt('Do you really want to continue?'))) {
+ // prompt the user. This message most make sense for but fe and fu.
+ if (!$skip_confirmation && !drush_confirm(dt('Module located at !dir will be updated. Do you want to continue?', array('!dir' => $directory)))) {
drush_die('Aborting.');
}
}
@@ -725,8 +752,13 @@ function drush_features_revert() {
$dt_args['@component'] = $component;
$confirmation_message = 'Do you really want to revert @module.@component?';
if ($skip_confirmation || drush_confirm(dt($confirmation_message, $dt_args))) {
- features_revert(array($module => array($component)));
- drush_log(dt('Reverted @module.@component.', $dt_args), 'ok');
+ if (features_feature_is_locked($module, $component)) {
+ drush_log(dt('Skipping locked @module.@component.', $dt_args), 'ok');
+ }
+ else {
+ features_revert(array($module => array($component)));
+ drush_log(dt('Reverted @module.@component.', $dt_args), 'ok');
+ }
}
else {
drush_log(dt('Skipping @module.@component.', $dt_args), 'ok');
@@ -882,6 +914,62 @@ function drush_features_diff() {
}
}
+/**
+ * Diff all enabled features that are not in their default state.
+ *
+ * @param ...
+ * (Optional) A list of features to exclude from being reverted.
+ */
+function drush_features_diff_all() {
+ module_load_include('inc', 'features', 'features.export');
+ $features_to_exclude = func_get_args();
+
+ $features_to_revert = array();
+ foreach (features_get_features(NULL, TRUE) as $module) {
+ if ($module->status && !in_array($module->name, $features_to_exclude)) {
+ switch (features_get_storage($module->name)) {
+ case FEATURES_OVERRIDDEN:
+ case FEATURES_NEEDS_REVIEW:
+ case FEATURES_REBUILDABLE:
+ $features_to_diff[] = $module->name;
+ break;
+ }
+ }
+ }
+
+ if ($features_to_diff) {
+
+ // Check if the user wants to apply the force option.
+ $force = drush_get_option('force');
+
+ if($force) {
+ foreach ($features_to_diff as $module) {
+
+ drush_print(dt('Diff for !module:', array('!module' => $module)));
+
+ drush_invoke_process(drush_sitealias_get_record('@self'), 'features-diff', array($module));
+ }
+ }
+ else {
+
+ drush_print(dt('A diff will be performed for the following modules: !modules',
+ array('!modules' => implode(', ', $features_to_diff))
+ ));
+
+ if (drush_confirm(dt('Do you want to continue?'))) {
+ foreach ($features_to_diff as $module) {
+ if (drush_confirm(dt('Diff !module?', array('!module' => $module)))) {
+ drush_invoke_process(drush_sitealias_get_record('@self'), 'features-diff', array($module));
+ }
+ }
+ }
+ else {
+ return drush_user_abort('Aborting.');
+ }
+ }
+ }
+}
+
/**
* Helper function to call drush_set_error().
*
diff --git a/sites/all/modules/contrib/admin/features/features.export.inc b/sites/all/modules/contrib/admin/features/features.export.inc
index 2f32c95c..154b26bc 100644
--- a/sites/all/modules/contrib/admin/features/features.export.inc
+++ b/sites/all/modules/contrib/admin/features/features.export.inc
@@ -45,11 +45,11 @@ function features_populate($info, $module_name) {
* @return fully populated $export array.
*/
function _features_populate($pipe, &$export, $module_name = '', $reset = FALSE) {
- static $processed = array();
- features_include();
if ($reset) {
- $processed = array();
+ drupal_static_reset(__FUNCTION__);
}
+ $processed = &drupal_static(__FUNCTION__, array());
+ features_include();
foreach ($pipe as $component => $data) {
// Convert already defined items to dependencies.
// _features_resolve_dependencies($data, $export, $module_name, $component);
@@ -385,10 +385,7 @@ function features_export_render($export, $module_name, $reset = FALSE) {
* Detect differences between DB and code components of a feature.
*/
function features_detect_overrides($module) {
- static $cache;
- if (!isset($cache)) {
- $cache = array();
- }
+ $cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$module->name])) {
// Rebuild feature from .info file description and prepare an export from current DB state.
$export = features_populate($module->info, $module->name);
@@ -713,10 +710,10 @@ function features_semaphore($op, $component) {
* Get normal objects for a given module/component pair.
*/
function features_get_normal($component, $module_name, $reset = FALSE) {
- static $cache;
- if (!isset($cache) || $reset) {
- $cache = array();
+ if ($reset) {
+ drupal_static_reset(__FUNCTION__);
}
+ $cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$module_name][$component])) {
features_include();
$code = NULL;
@@ -746,7 +743,7 @@ function features_get_normal($component, $module_name, $reset = FALSE) {
* Get defaults for a given module/component pair.
*/
function features_get_default($component, $module_name = NULL, $alter = TRUE, $reset = FALSE) {
- static $cache = array();
+ $cache = &drupal_static(__FUNCTION__, array());
$alter = !empty($alter); // ensure $alter is a true/false boolean
features_include();
features_include_defaults($component);
@@ -820,7 +817,7 @@ function features_get_default($component, $module_name = NULL, $alter = TRUE, $r
* Get a map of components to their providing modules.
*/
function features_get_default_map($component, $attribute = NULL, $callback = NULL, $reset = FALSE) {
- static $map = array();
+ $map = &drupal_static(__FUNCTION__, array());
global $features_ignore_conflicts;
if ($features_ignore_conflicts) {
@@ -866,10 +863,10 @@ function features_get_default_map($component, $attribute = NULL, $callback = NUL
* Retrieve an array of features/components and their current states.
*/
function features_get_component_states($features = array(), $rebuild_only = TRUE, $reset = FALSE) {
- static $cache;
- if (!isset($cache) || $reset) {
- $cache = array();
+ if ($reset) {
+ drupal_static_reset(__FUNCTION__);
}
+ $cache = &drupal_static(__FUNCTION__, array());
$all_features = features_get_features();
$features = !empty($features) ? $features : array_keys($all_features);
diff --git a/sites/all/modules/contrib/admin/features/features.info b/sites/all/modules/contrib/admin/features/features.info
index f563a4b1..205f2c8f 100644
--- a/sites/all/modules/contrib/admin/features/features.info
+++ b/sites/all/modules/contrib/admin/features/features.info
@@ -6,9 +6,9 @@ files[] = tests/features.test
configure = admin/structure/features/settings
-; Information added by drupal.org packaging script on 2013-10-17
-version = "7.x-2.0+0-dev"
+; Information added by Drupal.org packaging script on 2015-04-13
+version = "7.x-2.5"
core = "7.x"
project = "features"
-datestamp = "1382036080"
+datestamp = "1428944073"
diff --git a/sites/all/modules/contrib/admin/features/features.install b/sites/all/modules/contrib/admin/features/features.install
index 762a054f..9b1fd901 100644
--- a/sites/all/modules/contrib/admin/features/features.install
+++ b/sites/all/modules/contrib/admin/features/features.install
@@ -25,6 +25,16 @@ function features_uninstall() {
variable_del('features_default_export_path');
variable_del('features_semaphore');
variable_del('features_ignored_orphans');
+ variable_del('features_feature_locked');
+ variable_del('features_lock_mode');
+
+ db_delete('variable')
+ ->condition('name', 'features_admin_show_component_%', 'LIKE')
+ ->execute();
+ db_delete('variable')
+ ->condition('name', 'features_component_locked_%', 'LIKE')
+ ->execute();variable_del('features_component_locked_' . $component);
+
if (db_table_exists('menu_custom')) {
db_delete('menu_custom')
->condition('menu_name', 'features')
@@ -53,7 +63,7 @@ function _features_install_menu() {
/**
* Update 6100: Set module on all feature node types to 'features'.
-
+ *
* This update can be re-run as needed to repair any node types that are not
* removed after disabling the associated feature.
*
diff --git a/sites/all/modules/contrib/admin/features/features.js b/sites/all/modules/contrib/admin/features/features.js
index 877285cc..023247a4 100644
--- a/sites/all/modules/contrib/admin/features/features.js
+++ b/sites/all/modules/contrib/admin/features/features.js
@@ -100,33 +100,6 @@ jQuery.fn.sortElements = (function(){
}).trigger('change');
});
- // Export form machine-readable JS
- $('.feature-name:not(.processed)', context).each(function() {
- $('.feature-name')
- .addClass('processed')
- .after(' ');
- if ($('.feature-module-name').val() === $('.feature-name').val().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/_+/g, '_') || $('.feature-module-name').val() === '') {
- $('.feature-module-name').parents('.form-item').hide();
- $('.feature-name').bind('keyup change', function() {
- var machine = $(this).val().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/_+/g, '_');
- if (machine !== '_' && machine !== '') {
- $('.feature-module-name').val(machine);
- $('.feature-module-name-suffix').empty().append(' Machine name: ' + machine + ' [').append($(''+ Drupal.t('Edit') +'').click(function() {
- $('.feature-module-name').parents('.form-item').show();
- $('.feature-module-name-suffix').hide();
- $('.feature-name').unbind('keyup');
- return false;
- })).append(']');
- }
- else {
- $('.feature-module-name').val(machine);
- $('.feature-module-name-suffix').text('');
- }
- });
- $('.feature-name').keyup();
- }
- });
-
//View info dialog
var infoDialog = $('#features-info-file');
if (infoDialog.length != 0) {
@@ -184,7 +157,6 @@ jQuery.fn.sortElements = (function(){
}
function updateComponentCountInfo(item, section) {
- console.log(section);
switch (section) {
case 'select':
var parent = $(item).closest('.features-export-list').siblings('.features-export-component');
diff --git a/sites/all/modules/contrib/admin/features/features.module b/sites/all/modules/contrib/admin/features/features.module
index ba5b9fe5..3743963b 100644
--- a/sites/all/modules/contrib/admin/features/features.module
+++ b/sites/all/modules/contrib/admin/features/features.module
@@ -167,6 +167,17 @@ function features_menu() {
'file' => 'features.admin.inc',
);
}
+
+ $items['admin/structure/features/%feature/lock'] = array(
+ 'title' => 'Lock',
+ 'description' => 'Lock a feature or components.',
+ 'page callback' => 'features_admin_lock',
+ 'page arguments' => array(3, 5, 6),
+ 'load arguments' => array(3, TRUE, TRUE),
+ 'access arguments' => array('administer features'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'features.admin.inc',
+ );
$items['admin/structure/features/%feature/status'] = array(
'title' => 'Status',
'description' => 'Javascript status call back.',
@@ -214,11 +225,11 @@ function features_theme() {
$items = array();
$items['features_module_status'] = array(
- 'variables' => array('module' => null, 'status' => null)
+ 'variables' => array('module' => NULL, 'status' => NULL)
) + $base;
$items['features_components'] = array(
- 'variables' => array('info' => null, 'sources' => null),
+ 'variables' => array('info' => NULL, 'sources' => NULL),
) + $base;
$items['features_component_key'] = $base;
@@ -227,7 +238,11 @@ function features_theme() {
) + $base;
$items['features_storage_link'] = array(
- 'variables' => array('storage' => null, 'text' => null, 'path' => null, 'options' => array()),
+ 'variables' => array('storage' => NULL, 'text' => NULL, 'path' => NULL, 'options' => array()),
+ ) + $base;
+
+ $items['features_lock_link'] = array(
+ 'variables' => array('feature' => NULL, 'component' => NULL, 'locked' => FALSE),
) + $base;
$items['features_form_components'] =
@@ -290,6 +305,11 @@ function features_permission() {
'description' => t('Allow feature exports to be generated and written directly to site.'),
'restrict access' => TRUE,
),
+ 'rename features' => array(
+ 'title' => t('Edit feature machine name'),
+ 'description' => t('Allows editing machine name of a disabled feature'),
+ 'restrict access' => TRUE,
+ ),
);
}
@@ -299,10 +319,10 @@ function features_permission() {
function features_help($path, $arg) {
switch ($path) {
case 'admin/help#features':
- $output = file_get_contents(drupal_get_path('module', 'features') .'/README.txt');
- return module_exists('markdown') ? filter_xss_admin(module_invoke('markdown', 'filter', 'process', 0, -1, $output)) : '
'. check_plain($output) .''; + $output = file_get_contents(drupal_get_path('module', 'features') . '/README.txt'); + return module_exists('markdown') ? filter_xss_admin(module_invoke('markdown', 'filter', 'process', 0, -1, $output)) : '
' . check_plain($output) . ''; case 'admin/build/features': - return '
'. t('A "Feature" is a certain type of Drupal module which contains a package of configuration that, when enabled, provides a new set of functionality for your Drupal site. Enable features by selecting the checkboxes below and clicking the Save configuration button. If the configuration of the feature has been changed its "State" will be either "overridden" or "needs review", otherwise it will be "default", indicating that the configuration has not been changed. Click on the state to see more details about the feature and its components.') .'
'; + return '' . t('A "Feature" is a certain type of Drupal module which contains a package of configuration that, when enabled, provides a new set of functionality for your Drupal site. Enable features by selecting the checkboxes below and clicking the Save configuration button. If the configuration of the feature has been changed its "State" will be either "overridden" or "needs review", otherwise it will be "default", indicating that the configuration has not been changed. Click on the state to see more details about the feature and its components.') . '
'; } } @@ -641,6 +661,10 @@ function features_get_info($type = 'module', $name = NULL, $reset = FALSE) { $files = system_rebuild_module_data(); foreach ($files as $row) { + // Remove modification timestamp, added in Drupal 7.33. + if (isset($row->info['mtime'])) { + unset($row->info['mtime']); + } // Avoid false-reported feature overrides for php = 5.2.4 line in .info file. if (isset($row->info['php'])) { unset($row->info['php']); @@ -654,7 +678,7 @@ function features_get_info($type = 'module', $name = NULL, $reset = FALSE) { if (!empty($row->info['features'])) { // Fix css/js paths if (!empty($row->info['stylesheets'])) { - foreach($row->info['stylesheets'] as $media => $css) { + foreach ($row->info['stylesheets'] as $media => $css) { $row->info['stylesheets'][$media] = array_keys($css); } } @@ -789,7 +813,7 @@ function features_get_conflicts($reset = FALSE) { if (isset($component_info[$type]['duplicates']) && $component_info[$type]['duplicates'] == FEATURES_DUPLICATES_ALLOWED) { continue; } - else if (count($modules) > 1) { + elseif (count($modules) > 1) { foreach ($modules as $module) { if (!isset($conflicts[$module])) { $conflicts[$module] = array(); @@ -840,7 +864,7 @@ function features_get_module_status($module) { if (module_exists($module)) { return FEATURES_MODULE_ENABLED; } - else if (features_get_modules($module)) { + elseif (features_get_modules($module)) { return FEATURES_MODULE_DISABLED; } else { @@ -886,6 +910,7 @@ function features_form_system_modules_alter(&$form) { * Restore the specified modules to the default state. */ function _features_restore($op, $items = array()) { + $lockable = FALSE; // Set this variable in $conf if having timeout issues during install/rebuild. if (variable_get('features_restore_time_limit_' . $op, FALSE) !== FALSE) { drupal_set_time_limit(variable_get('features_restore_time_limit_' . $op, FALSE)); @@ -899,11 +924,13 @@ function _features_restore($op, $items = array()) { $restore_states = array(FEATURES_OVERRIDDEN, FEATURES_REBUILDABLE, FEATURES_NEEDS_REVIEW); $restore_hook = 'features_revert'; $log_action = 'Revert'; + $lockable = TRUE; break; case 'rebuild': $restore_states = array(FEATURES_REBUILDABLE); $restore_hook = 'features_rebuild'; $log_action = 'Rebuild'; + $lockable = variable_get('features_lock_mode', 'all') == 'all'; break; case 'disable': $restore_hook = 'features_disable_feature'; @@ -932,8 +959,20 @@ function _features_restore($op, $items = array()) { } } + // Invoke global pre restore hook. + module_invoke_all('features_pre_restore', $op, $items); foreach ($items as $module_name => $components) { + // If feature is totally locked, do not execute past this stage. + if ($lockable && features_feature_is_locked($module_name)) { + watchdog('features', 'Tried @actioning a locked @module_name, aborted.', array('@action' => $log_action, '@module_name' => $module_name)); + continue; + } foreach ($components as $component) { + // If feature is totally locked, do not execute past this stage. + if ($lockable && features_feature_is_locked($module_name, $component)) { + watchdog('features', 'Tried @actioning a locked @module_name / @component, aborted.', array('@action' => $log_action, '@component' => $component, '@module_name' => $module_name)); + continue; + } // Invoke pre hook $pre_hook = 'pre_' . $restore_hook; module_invoke($module_name, $pre_hook, $component); @@ -955,6 +994,8 @@ function _features_restore($op, $items = array()) { module_invoke($module_name, $post_hook, $component); } } + // Invoke global post restore hook. + module_invoke_all('features_post_restore', $op, $items); } /** @@ -1034,6 +1075,9 @@ function features_hook_info() { * Change vocabularies permission, from vocab id to machine name and vice versa. */ function _user_features_change_term_permission(&$perm, $type = 'vid') { + if (!module_exists('taxonomy')) { + return; + } // Export vocabulary permissions using the machine name, instead of vocabulary // id. if (strpos($perm, 'edit terms in ') !== FALSE || strpos($perm, 'delete terms in ') !== FALSE) { @@ -1109,3 +1153,53 @@ function features_get_deprecated($components = array()) { } return $deprecated; } + +/** + * Returns whether a feature or it's component is locked. + */ +function features_feature_is_locked($feature, $component = NULL, $check_global_component_setting = TRUE) { + $locked = variable_get('features_feature_locked', array()); + if ($component) { + return ($check_global_component_setting && features_component_is_locked($component)) || !empty($locked[$feature][$component]); + } + else { + return !empty($locked[$feature]['_all']); + } +} + +/** + * Returns whether a component is locked. + */ +function features_component_is_locked($component) { + return variable_get('features_component_locked_' . $component, FALSE); +} + +/** + * Locks a feature or it's component. + */ +function features_feature_lock($feature, $component = NULL) { + $locked = variable_get('features_feature_locked', array()); + $locked[$feature] = !empty($locked[$feature]) ? $locked[$feature] : array(); + if ($component) { + $locked[$feature][$component] = TRUE; + } + else { + $locked[$feature]['_all'] = TRUE; + } + variable_set('features_feature_locked', $locked); +} + + +/** + * Unlocks a feature or it's component. + */ +function features_feature_unlock($feature, $component = NULL) { + $locked = variable_get('features_feature_locked', array()); + if ($component) { + unset($locked[$feature][$component]); + } + else { + unset($locked[$feature]['_all']); + } + variable_set('features_feature_locked', $locked); +} diff --git a/sites/all/modules/contrib/admin/features/includes/features.field.inc b/sites/all/modules/contrib/admin/features/includes/features.field.inc index 09104a32..73332271 100644 --- a/sites/all/modules/contrib/admin/features/includes/features.field.inc +++ b/sites/all/modules/contrib/admin/features/includes/features.field.inc @@ -151,7 +151,7 @@ function field_base_features_export_render($module, $data, $export = NULL) { foreach ($data as $identifier) { if ($field = features_field_base_load($identifier)) { unset($field['columns']); - // unset($field['locked']); + unset($field['foreign keys']); // Only remove the 'storage' declaration if the field is using the default // storage type. if ($field['storage']['type'] == variable_get('field_storage_default', 'field_sql_storage')) { @@ -166,8 +166,15 @@ function field_base_features_export_render($module, $data, $export = NULL) { _field_instance_features_export_sort($field); $field_export = features_var_export($field, ' '); + $field_prefix = ' // Exported field_base: '; $field_identifier = features_var_export($identifier); - $code[] = " // Exported field_base: {$field_identifier}"; + if (features_field_export_needs_wrap($field_prefix, $field_identifier)) { + $code[] = rtrim($field_prefix); + $code[] = " // {$field_identifier}"; + } + else { + $code[] = $field_prefix . $field_identifier; + } $code[] = " \$field_bases[{$field_identifier}] = {$field_export};"; $code[] = ""; } @@ -190,8 +197,15 @@ function field_instance_features_export_render($module, $data, $export = NULL) { if ($instance = features_field_instance_load($identifier)) { _field_instance_features_export_sort($instance); $field_export = features_var_export($instance, ' '); + $instance_prefix = ' // Exported field_instance: '; $instance_identifier = features_var_export($identifier); - $code[] = " // Exported field_instance: {$instance_identifier}"; + if (features_field_export_needs_wrap($instance_prefix, $instance_identifier)) { + $code[] = rtrim($instance_prefix); + $code[] = " // {$instance_identifier}"; + } + else { + $code[] = $instance_prefix . $instance_identifier; + } $code[] = " \$field_instances[{$instance_identifier}] = {$field_export};"; $code[] = ""; @@ -528,3 +542,23 @@ function features_field_load($identifier) { } return FALSE; } + +/** + * Determine if a field export line needs to be wrapped. + * + * Drupal code standards specify that comments should wrap at 80 characters or + * less. + * + * @param string $prefix + * The prefix to be exported before the field identifier. + * @param string $identifier + * The field identifier. + * + * @return BOOL + * TRUE if the line should be wrapped after the prefix, else FALSE. + * + * @see https://www.drupal.org/node/1354 + */ +function features_field_export_needs_wrap($prefix, $identifier) { + return (strlen($prefix) + strlen($identifier) > 80); +} diff --git a/sites/all/modules/contrib/admin/features/includes/features.image.inc b/sites/all/modules/contrib/admin/features/includes/features.image.inc index 2b5eb273..b2058b7c 100644 --- a/sites/all/modules/contrib/admin/features/includes/features.image.inc +++ b/sites/all/modules/contrib/admin/features/includes/features.image.inc @@ -86,16 +86,25 @@ function image_features_revert($module) { /** * Remove unnecessary keys for export. */ -function _image_features_style_sanitize(&$style, $child = FALSE) { - $omit = $child ? array('isid', 'ieid', 'storage') : array('isid', 'ieid', 'storage', 'module'); - if (is_array($style)) { - foreach ($style as $k => $v) { - if (in_array($k, $omit, TRUE)) { - unset($style[$k]); - } - else if (is_array($v)) { - _image_features_style_sanitize($style[$k], TRUE); - } - } +function _image_features_style_sanitize(array &$style) { + // Sanitize style: Don't export numeric IDs and things which get overwritten + // in image_styles() or are code/storage specific. The name property will be + // the key of the exported $style array. + $style = array_diff_key($style, array_flip(array( + 'isid', + 'name', + 'module', + 'storage', + ))); + + // Sanitize effects: all that needs to be kept is name, weight and data, + // which holds all the style-specific configuration. Other keys are assumed + // to belong to the definition of the effect itself, so not configuration. + foreach ($style['effects'] as $id => $effect) { + $style['effects'][$id] = array_intersect_key($effect, array_flip(array( + 'name', + 'data', + 'weight', + ))); } } diff --git a/sites/all/modules/contrib/admin/features/includes/features.locale.inc b/sites/all/modules/contrib/admin/features/includes/features.locale.inc index fc27174b..126d177d 100644 --- a/sites/all/modules/contrib/admin/features/includes/features.locale.inc +++ b/sites/all/modules/contrib/admin/features/includes/features.locale.inc @@ -134,6 +134,7 @@ function _features_language_save($language) { ->fields(array( 'plurals' => empty($language->plurals) ? 0 : $language->plurals, 'formula' => empty($language->formula) ? '' : $language->formula, + 'weight' => empty($language->weight) ? 0 : $language->weight, )) ->condition('language', $language->language) ->execute(); diff --git a/sites/all/modules/contrib/admin/features/includes/features.menu.inc b/sites/all/modules/contrib/admin/features/includes/features.menu.inc index a11350ff..c883e6ac 100644 --- a/sites/all/modules/contrib/admin/features/includes/features.menu.inc +++ b/sites/all/modules/contrib/admin/features/includes/features.menu.inc @@ -241,7 +241,8 @@ function menu_links_features_export_render($module, $data, $export = NULL) { // Don't show new identifier unless we are actually exporting. $link['options']['identifier'] = $new_identifier; // identifiers are renewed, => that means we need to update them in the db - menu_link_save($temp = $link); + $temp = $link; + menu_link_save($temp); } unset($link['plid']); @@ -295,19 +296,27 @@ function menu_links_features_rebuild_ordered($menu_links, $reset = FALSE) { foreach ($unordered as $key => $link) { $identifier = menu_links_features_identifier($link); $parent = isset($link['parent_identifier']) ? $link['parent_identifier'] : ''; - if (empty($parent)) { - $ordered[$identifier] = 0; - $all_links[$identifier] = $link; - unset($unordered[$key]); + $weight = 0; + // Parent has been seen, so weigh this above parent. + if (isset($ordered[$parent])) { + $weight = $ordered[$parent] + 1; } - elseif (isset($ordered[$parent])) { - $ordered[$identifier] = $ordered[$parent] + 1; - $all_links[$identifier] = $link; - unset($unordered[$key]); + // Next loop will try to find parent weight instead. + elseif ($parent) { + continue; } + $ordered[$identifier] = $weight; + $all_links[$identifier] = $link; + unset($unordered[$key]); } + // Exit out when the above does no changes this loop. } while (count($unordered) < $current); } + // Add all remaining unordered items to the ordered list. + foreach ($unordered as $link) { + $identifier = menu_links_features_identifier($link); + $ordered[$identifier] = 0; + } asort($ordered); } @@ -356,10 +365,11 @@ function features_menu_link_load($identifier) { list($menu_name, $link_path) = explode(':', $identifier, 2); } $links = db_select('menu_links') - ->fields('menu_links', array('menu_name', 'mlid', 'plid', 'link_path', 'router_path', 'link_title', 'options', 'module', 'hidden', 'external', 'has_children', 'expanded', 'weight', 'customized')) - ->condition('menu_name', $menu_name) - ->condition('link_path', $link_path) - ->execute() + ->fields('menu_links', array('menu_name', 'mlid', 'plid', 'link_path', 'router_path', 'link_title', 'options', 'module', 'hidden', 'external', 'has_children', 'expanded', 'weight', 'customized')) + ->condition('menu_name', $menu_name) + ->condition('link_path', $link_path) + ->addTag('features_menu_link') + ->execute() ->fetchAllAssoc('mlid'); foreach($links as $link) { @@ -391,11 +401,14 @@ function features_menu_link_load($identifier) { foreach($links as $link) { $link->options = unserialize($link->options); - - // title or previous identifier matches - if ((isset($link->options['identifier']) && strcmp($link->options['identifier'], $identifier) == 0) - || (isset($clean_title) && strcmp(features_clean_title($link->link_title), $clean_title) == 0)) { - + // Links with a stored identifier must only be matched on that identifier, + // to prevent cross over assumptions. + if (isset($link->options['identifier'])) { + if (strcmp($link->options['identifier'], $identifier) == 0) { + return (array)$link; + } + } + elseif ((strcmp(features_clean_title($link->link_title), $clean_title) == 0)) { return (array)$link; } } diff --git a/sites/all/modules/contrib/admin/features/includes/features.node.inc b/sites/all/modules/contrib/admin/features/includes/features.node.inc index 7beb55fb..f40341af 100644 --- a/sites/all/modules/contrib/admin/features/includes/features.node.inc +++ b/sites/all/modules/contrib/admin/features/includes/features.node.inc @@ -9,6 +9,7 @@ function node_features_api() { 'name' => t('Content types'), 'feature_source' => TRUE, 'default_hook' => 'node_info', + 'alter_type' => FEATURES_ALTER_TYPE_INLINE, ), ); } @@ -86,6 +87,7 @@ function node_features_export_render($module, $data, $export = NULL) { } } $output[] = ' );'; + $output[] = ' drupal_alter(\'node_info\', $items);'; $output[] = ' return $items;'; $output = implode("\n", $output); return array('node_info' => $output); @@ -115,7 +117,7 @@ function node_features_revert($module = NULL) { } /** - * Implements hook_features_disable(). + * Implements hook_features_disable_feature(). * * When a features module is disabled, modify any node types it provides so * they can be deleted manually through the content types UI. @@ -123,7 +125,7 @@ function node_features_revert($module = NULL) { * @param $module * Name of module that has been disabled. */ -function node_features_disable($module) { +function node_features_disable_feature($module) { if ($default_types = features_get_default('node', $module)) { foreach ($default_types as $type_name => $type_info) { $type_info = node_type_load($type_name); @@ -131,13 +133,14 @@ function node_features_disable($module) { $type_info->custom = 1; $type_info->modified = 1; $type_info->locked = 0; + $type_info->disabled = 0; node_type_save($type_info); } } } /** - * Implements hook_features_enable(). + * Implements hook_features_enable_feature(). * * When a features module is enabled, modify any node types it provides so * they can no longer be deleted manually through the content types UI. @@ -145,7 +148,7 @@ function node_features_disable($module) { * @param $module * Name of module that has been enabled. */ -function node_features_enable($module) { +function node_features_enable_feature($module) { if ($default_types = features_get_default('node', $module)) { foreach ($default_types as $type_name => $type_info) { // Ensure the type exists. @@ -154,6 +157,7 @@ function node_features_enable($module) { $type_info->custom = 0; $type_info->modified = 0; $type_info->locked = 1; + $type_info->disabled = 0; node_type_save($type_info); } } diff --git a/sites/all/modules/contrib/admin/features/includes/features.user.inc b/sites/all/modules/contrib/admin/features/includes/features.user.inc index 2805a774..152c5a82 100644 --- a/sites/all/modules/contrib/admin/features/includes/features.user.inc +++ b/sites/all/modules/contrib/admin/features/includes/features.user.inc @@ -50,12 +50,12 @@ function user_permission_features_export_options() { $modules = array(); $module_info = system_get_info('module'); foreach (module_implements('permission') as $module) { - $modules[$module_info[$module]['name']] = $module; + $modules[$module] = $module_info[$module]['name']; } ksort($modules); $options = array(); - foreach ($modules as $display_name => $module) { + foreach ($modules as $module => $display_name) { if ($permissions = module_invoke($module, 'permission')) { foreach ($permissions as $perm => $perm_item) { // Export vocabulary permissions using the machine name, instead of diff --git a/sites/all/modules/contrib/admin/features/tests/features_test/features_test.features.inc b/sites/all/modules/contrib/admin/features/tests/features_test/features_test.features.inc index 38aaa4d4..8d0646ff 100644 --- a/sites/all/modules/contrib/admin/features/tests/features_test/features_test.features.inc +++ b/sites/all/modules/contrib/admin/features/tests/features_test/features_test.features.inc @@ -29,16 +29,8 @@ function features_test_image_default_styles() { // Exported image style: features_test. $styles['features_test'] = array( - 'name' => 'features_test', 'effects' => array( 2 => array( - 'label' => 'Scale', - 'help' => 'Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.', - 'effect callback' => 'image_scale_effect', - 'dimensions callback' => 'image_scale_dimensions', - 'form callback' => 'image_scale_form', - 'summary theme' => 'image_scale_summary', - 'module' => 'image', 'name' => 'image_scale', 'data' => array( 'width' => 100, diff --git a/sites/all/modules/contrib/admin/features/tests/features_test/features_test.info b/sites/all/modules/contrib/admin/features/tests/features_test/features_test.info index 382e1505..d27d18ce 100644 --- a/sites/all/modules/contrib/admin/features/tests/features_test/features_test.info +++ b/sites/all/modules/contrib/admin/features/tests/features_test/features_test.info @@ -21,9 +21,9 @@ features[user_permission][] = create features_test content features[views_view][] = features_test hidden = 1 -; Information added by drupal.org packaging script on 2013-10-17 -version = "7.x-2.0+0-dev" +; Information added by Drupal.org packaging script on 2015-04-13 +version = "7.x-2.5" core = "7.x" project = "features" -datestamp = "1382036080" +datestamp = "1428944073" diff --git a/sites/all/modules/contrib/admin/features/theme/features-admin-components.tpl.php b/sites/all/modules/contrib/admin/features/theme/features-admin-components.tpl.php index 69faadfb..04213a0f 100644 --- a/sites/all/modules/contrib/admin/features/theme/features-admin-components.tpl.php +++ b/sites/all/modules/contrib/admin/features/theme/features-admin-components.tpl.php @@ -3,7 +3,7 @@ga("create", "UA-XXXX-Y", {"name":"value"});
. For more information, read create only fields documentation in the Analytics.js field reference.', array('@url' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create')),
+ '#element_validate' => array('googleanalytics_validate_create_field_values'),
);
$form['advanced']['codesnippet']['googleanalytics_codesnippet_before'] = array(
'#type' => 'textarea',
'#title' => t('Code snippet (before)'),
'#default_value' => variable_get('googleanalytics_codesnippet_before', ''),
'#rows' => 5,
- '#description' => t("Code in this textarea will be added before _gaq.push(['_trackPageview'])."),
+ '#description' => t('Code in this textarea will be added before ga("send", "pageview");
.'),
);
$form['advanced']['codesnippet']['googleanalytics_codesnippet_after'] = array(
'#type' => 'textarea',
'#title' => t('Code snippet (after)'),
'#default_value' => variable_get('googleanalytics_codesnippet_after', ''),
'#rows' => 5,
- '#description' => t("Code in this textarea will be added after _gaq.push(['_trackPageview']). This is useful if you'd like to track a site in two accounts."),
+ '#description' => t('Code in this textarea will be added after ga("send", "pageview");
. This is useful if you\'d like to track a site in two accounts.'),
);
- $form['advanced']['googleanalytics_js_scope'] = array(
- '#type' => 'select',
- '#title' => t('JavaScript scope'),
- '#description' => t('Google recommends adding the external JavaScript files to the header for performance reasons. If Multiple top-level domains has been selected, this setting will be forced to header.'),
- '#options' => array(
- 'footer' => t('Footer'),
- 'header' => t('Header'),
- ),
- '#default_value' => variable_get('googleanalytics_js_scope', 'header'),
- '#disabled' => (variable_get('googleanalytics_domain_mode', 0) == 2) ? TRUE : FALSE,
+ $form['advanced']['googleanalytics_debug'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable debugging'),
+ '#description' => t('If checked, the Google Universal Analytics debugging script will be loaded. You should not enable your production site to use this version of the JavaScript. The analytics_debug.js script is larger than the analytics.js tracking code and it is not typically cached. Using it in your production site will slow down your site for all of your users. Again, this is only for your own testing purposes. Debug messages are printed to the window.console
object.'),
+ '#default_value' => variable_get('googleanalytics_debug', 0),
);
return system_settings_form($form);
@@ -408,31 +472,37 @@ function googleanalytics_admin_settings_form($form_state) {
* Implements _form_validate().
*/
function googleanalytics_admin_settings_form_validate($form, &$form_state) {
- // Custom variables validation.
- foreach ($form_state['values']['googleanalytics_custom_var']['slots'] as $custom_var) {
- $form_state['values']['googleanalytics_custom_var']['slots'][$custom_var['slot']]['name'] = trim($custom_var['name']);
- $form_state['values']['googleanalytics_custom_var']['slots'][$custom_var['slot']]['value'] = trim($custom_var['value']);
-
- // Validate empty names/values.
- if (empty($custom_var['name']) && !empty($custom_var['value'])) {
- form_set_error("googleanalytics_custom_var][slots][" . $custom_var['slot'] . "][name", t('The custom variable @slot-number requires a Name if a Value has been provided.', array('@slot-number' => $custom_var['slot'])));
- }
- elseif (!empty($custom_var['name']) && empty($custom_var['value'])) {
- form_set_error("googleanalytics_custom_var][slots][" . $custom_var['slot'] . "][value", t('The custom variable @slot-number requires a Value if a Name has been provided.', array('@slot-number' => $custom_var['slot'])));
+ // Trim custom dimensions and metrics.
+ foreach ($form_state['values']['googleanalytics_custom_dimension']['indexes'] as $dimension) {
+ $form_state['values']['googleanalytics_custom_dimension']['indexes'][$dimension['index']]['value'] = trim($dimension['value']);
+ // Remove empty values from the array.
+ if (!drupal_strlen($form_state['values']['googleanalytics_custom_dimension']['indexes'][$dimension['index']]['value'])) {
+ unset($form_state['values']['googleanalytics_custom_dimension']['indexes'][$dimension['index']]);
}
}
+ $form_state['values']['googleanalytics_custom_dimension'] = $form_state['values']['googleanalytics_custom_dimension']['indexes'];
+
+ foreach ($form_state['values']['googleanalytics_custom_metric']['indexes'] as $metric) {
+ $form_state['values']['googleanalytics_custom_metric']['indexes'][$metric['index']]['value'] = trim($metric['value']);
+ // Remove empty values from the array.
+ if (!drupal_strlen($form_state['values']['googleanalytics_custom_metric']['indexes'][$metric['index']]['value'])) {
+ unset($form_state['values']['googleanalytics_custom_metric']['indexes'][$metric['index']]);
+ }
+ }
+ $form_state['values']['googleanalytics_custom_metric'] = $form_state['values']['googleanalytics_custom_metric']['indexes'];
// Trim some text values.
$form_state['values']['googleanalytics_account'] = trim($form_state['values']['googleanalytics_account']);
$form_state['values']['googleanalytics_pages'] = trim($form_state['values']['googleanalytics_pages']);
$form_state['values']['googleanalytics_cross_domains'] = trim($form_state['values']['googleanalytics_cross_domains']);
+ $form_state['values']['googleanalytics_codesnippet_create'] = _googleanalytics_extract_create_field_values($form_state['values']['googleanalytics_codesnippet_create']);
$form_state['values']['googleanalytics_codesnippet_before'] = trim($form_state['values']['googleanalytics_codesnippet_before']);
$form_state['values']['googleanalytics_codesnippet_after'] = trim($form_state['values']['googleanalytics_codesnippet_after']);
// Replace all type of dashes (n-dash, m-dash, minus) with the normal dashes.
$form_state['values']['googleanalytics_account'] = str_replace(array('–', '—', '−'), '-', $form_state['values']['googleanalytics_account']);
- if (!preg_match('/^UA-\d{4,}-\d+$/', $form_state['values']['googleanalytics_account'])) {
+ if (!preg_match('/^UA-\d+-\d+$/', $form_state['values']['googleanalytics_account'])) {
form_set_error('googleanalytics_account', t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'));
}
@@ -440,16 +510,25 @@ function googleanalytics_admin_settings_form_validate($form, &$form_state) {
if ($form_state['values']['googleanalytics_domain_mode'] == 2 && empty($form_state['values']['googleanalytics_cross_domains'])) {
form_set_error('googleanalytics_cross_domains', t('A list of top-level domains is required if Multiple top-level domains has been selected.'));
}
+ // Clear the domain list if cross domains are disabled.
+ if ($form_state['values']['googleanalytics_domain_mode'] != 2) {
+ $form_state['values']['googleanalytics_cross_domains'] = '';
+ }
+
+ // Disallow empty list of download file extensions.
+ if ($form_state['values']['googleanalytics_trackfiles'] && empty($form_state['values']['googleanalytics_trackfiles_extensions'])) {
+ form_set_error('googleanalytics_trackfiles_extensions', t('List of download file extensions cannot empty.'));
+ }
// Clear obsolete local cache if cache has been disabled.
if (empty($form_state['values']['googleanalytics_cache']) && $form['advanced']['googleanalytics_cache']['#default_value']) {
googleanalytics_clear_js_cache();
}
// This is for the Newbie's who cannot read a text area description.
- if (stristr($form_state['values']['googleanalytics_codesnippet_before'], 'google-analytics.com/ga.js')) {
+ if (stristr($form_state['values']['googleanalytics_codesnippet_before'], 'google-analytics.com/analytics.js')) {
form_set_error('googleanalytics_codesnippet_before', t('Do not add the tracker code provided by Google into the javascript code snippets! This module already builds the tracker code based on your Google Analytics account number and settings.'));
}
- if (stristr($form_state['values']['googleanalytics_codesnippet_after'], 'google-analytics.com/ga.js')) {
+ if (stristr($form_state['values']['googleanalytics_codesnippet_after'], 'google-analytics.com/analytics.js')) {
form_set_error('googleanalytics_codesnippet_after', t('Do not add the tracker code provided by Google into the javascript code snippets! This module already builds the tracker code based on your Google Analytics account number and settings.'));
}
if (preg_match('/(.*)<\/?script(.*)>(.*)/i', $form_state['values']['googleanalytics_codesnippet_before'])) {
@@ -458,11 +537,6 @@ function googleanalytics_admin_settings_form_validate($form, &$form_state) {
if (preg_match('/(.*)<\/?script(.*)>(.*)/i', $form_state['values']['googleanalytics_codesnippet_after'])) {
form_set_error('googleanalytics_codesnippet_after', t('Do not include the <script> tags in the javascript code snippets.'));
}
-
- // Header section must be forced for multiple top-level domains.
- if ($form_state['values']['googleanalytics_domain_mode'] == 2) {
- $form_state['values']['googleanalytics_js_scope'] = 'header';
- }
}
/**
@@ -472,27 +546,25 @@ function theme_googleanalytics_admin_custom_var_table($variables) {
$form = $variables['form'];
$header = array(
- array('data' => t('Slot')),
- array('data' => t('Name')),
+ array('data' => t('Index')),
array('data' => t('Value')),
- array('data' => t('Scope')),
);
$rows = array();
- foreach (element_children($form['slots']) as $key => $id) {
+ foreach (element_children($form['indexes']) as $key => $id) {
$rows[] = array(
'data' => array(
- drupal_render($form['slots'][$id]['slot']),
- drupal_render($form['slots'][$id]['name']),
- drupal_render($form['slots'][$id]['value']),
- drupal_render($form['slots'][$id]['scope']),
+ drupal_render($form['indexes'][$id]['index']),
+ drupal_render($form['indexes'][$id]['value']),
),
);
}
$output = theme('table', array('header' => $header, 'rows' => $rows));
- $output .= drupal_render($form['googleanalytics_custom_var_description']);
- $output .= drupal_render($form['googleanalytics_custom_var_token_tree']);
+ $output .= drupal_render($form['googleanalytics_description']);
+ if (isset($form['googleanalytics_token_tree'])) {
+ $output .= drupal_render($form['googleanalytics_token_tree']);
+ }
return $output;
}
@@ -599,3 +671,198 @@ function _googleanalytics_contains_forbidden_token($token_string) {
return preg_match('/' . implode('|', array_map('preg_quote', $token_blacklist)) . '/i', $token_string);
}
+
+/**
+ * #element_validate callback for create only fields.
+ *
+ * @param $element
+ * An associative array containing the properties and children of the
+ * generic form element.
+ * @param $form_state
+ * The $form_state array for the form this element belongs to.
+ *
+ * @see form_process_pattern()
+ */
+function googleanalytics_validate_create_field_values(&$element, &$form_state) {
+ $values = _googleanalytics_extract_create_field_values($element['#value']);
+
+ if (!is_array($values)) {
+ form_error($element, t('The %element-title field contains invalid input.', array('%element-title' => $element['#title'])));
+ }
+ else {
+ // Check that name and value are valid for the field type.
+ foreach ($values as $name => $value) {
+ if ($error = _googleanalytics_validate_create_field_name($name)) {
+ form_error($element, $error);
+ break;
+ }
+ if ($error = _googleanalytics_validate_create_field_value($value)) {
+ form_error($element, $error);
+ break;
+ }
+ }
+
+ return $element;
+ }
+}
+
+/**
+ * Extracts the values array from the element.
+ *
+ * @param string $string
+ * The raw string to extract values from.
+ *
+ * @return array|null
+ * The array of extracted key/value pairs, or NULL if the string is invalid.
+ *
+ * @see \Drupal\options\Plugin\Field\FieldType\ListTextItem::allowedValuesString()
+ */
+function _googleanalytics_extract_create_field_values($string) {
+ $values = array();
+
+ $list = explode("\n", $string);
+ $list = array_map('trim', $list);
+ $list = array_filter($list, 'strlen');
+
+ foreach ($list as $position => $text) {
+ // Check for an explicit key.
+ $matches = array();
+ if (preg_match('/(.*)\|(.*)/', $text, $matches)) {
+ // Trim key and value to avoid unwanted spaces issues.
+ $name = trim($matches[1]);
+ $value = trim($matches[2]);
+ }
+ else {
+ return;
+ }
+
+ $values[$name] = $value;
+ }
+
+ return _googleanalytics_convert_form_value_data_types($values);
+}
+
+/**
+ * Checks whether a field name is valid.
+ *
+ * @param string $name
+ * The option value entered by the user.
+ *
+ * @return string
+ * The error message if the specified value is invalid, NULL otherwise.
+ */
+function _googleanalytics_validate_create_field_name($name) {
+ // List of supported field names:
+ // https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create
+ $create_only_fields = array(
+ 'clientId',
+ 'userId',
+ 'sampleRate',
+ 'siteSpeedSampleRate',
+ 'alwaysSendReferrer',
+ 'allowAnchor',
+ 'cookieName',
+ 'cookieDomain',
+ 'cookieExpires',
+ 'legacyCookieDomain',
+ );
+
+ if ($name == 'name') {
+ return t('Create only field name %name is a disallowed field name. Changing the Tracker Name is currently not supported.', array('%name' => $name));
+ }
+ if ($name == 'allowLinker') {
+ return t('Create only field name %name is a disallowed field name. Please select Multiple top-level domains under What are you tracking to enable cross domain tracking.', array('%name' => $name));
+ }
+ if (!in_array($name, $create_only_fields)) {
+ return t('Create only field name %name is an unknown field name. Field names are case sensitive. Please see create only fields documentation for supported field names.', array('%name' => $name, '@url' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create'));
+ }
+}
+
+/**
+ * Checks whether a field value is valid.
+ *
+ * @param string $value
+ * The option value entered by the user.
+ *
+ * @return string
+ * The error message if the specified value is invalid, NULL otherwise.
+ */
+function _googleanalytics_validate_create_field_value($value) {
+ if (!is_bool($value) && !drupal_strlen($value)) {
+ return t('A create only field requires a value.');
+ }
+ if (drupal_strlen($value) > 255) {
+ return t('The value of a create only field must be a string at most 255 characters long.');
+ }
+}
+
+/**
+ * Generates a string representation of an array.
+ *
+ * This string format is suitable for edition in a textarea.
+ *
+ * @param array $values
+ * An array of values, where array keys are values and array values are
+ * labels.
+ *
+ * @return string
+ * The string representation of the $values array:
+ * - Values are separated by a carriage return.
+ * - Each value is in the format "name|value" or "value".
+ */
+function _googleanalytics_get_name_value_string($values) {
+ $lines = array();
+ foreach ($values as $name => $value) {
+ // Convert data types.
+ // @todo: #2251377: Json utility class serializes boolean values to incorrect data type
+ if (is_bool($value)) {
+ $value = ($value) ? 'true' : 'false';
+ }
+
+ $lines[] = "$name|$value";
+ }
+ return implode("\n", $lines);
+}
+
+/**
+ * Prepare form data types for Json conversion.
+ *
+ * @param array $values
+ * Array of name/value pairs.
+ *
+ * @return array
+ * Array of name/value pairs with casted data types.
+ */
+function _googleanalytics_convert_form_value_data_types($values) {
+
+ foreach ($values as $name => $value) {
+ // Convert data types.
+ // @todo: #2251377: Json utility class serializes boolean values to incorrect data type
+ $match = drupal_strtolower($value);
+ if ($match == 'true') {
+ $value = TRUE;
+ }
+ elseif ($match == 'false') {
+ $value = FALSE;
+ }
+
+ // Convert other known fields.
+ // @todo: #2251343: Json utility class serializes numeric values to incorrect data type
+ switch ($name) {
+ case 'sampleRate':
+ // Float
+ settype($value, 'float');
+ break;
+
+ case 'siteSpeedSampleRate':
+ case 'cookieExpires':
+ // Integer
+ settype($value, 'integer');
+ break;
+ }
+
+ $values[$name] = $value;
+ }
+
+ return $values;
+}
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.js b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.js
index 35da84e2..083eeaa3 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.js
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.js
@@ -91,7 +91,7 @@ Drupal.behaviors.trackingSettingsSummary = {
vals.push(Drupal.t('AdSense ads'));
}
if ($('input#edit-googleanalytics-trackdoubleclick', context).is(':checked')) {
- vals.push(Drupal.t('DoubleClick data'));
+ vals.push(Drupal.t('Display features'));
}
if (!vals.length) {
return Drupal.t('Not tracked');
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.debug.js b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.debug.js
new file mode 100644
index 00000000..861f7fdb
--- /dev/null
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.debug.js
@@ -0,0 +1,183 @@
+(function ($) {
+
+Drupal.googleanalytics = {};
+
+$(document).ready(function() {
+
+ // Attach mousedown, keyup, touchstart events to document only and catch
+ // clicks on all elements.
+ $(document.body).bind("mousedown keyup touchstart", function(event) {
+ console.group("Running Google Analytics for Drupal.");
+ console.info(event);
+
+ // Catch the closest surrounding link of a clicked element.
+ $(event.target).closest("a,area").each(function() {
+ console.info("Element '%o' has been detected. Link '%s' found.", this, this.href);
+
+ // Is the clicked URL internal?
+ if (Drupal.googleanalytics.isInternal(this.href)) {
+ // Skip 'click' tracking, if custom tracking events are bound.
+ if ($(this).is('.colorbox')) {
+ // Do nothing here. The custom event will handle all tracking.
+ console.info("Click on .colorbox item has been detected.");
+ }
+ // Is download tracking activated and the file extension configured for download tracking?
+ else if (Drupal.settings.googleanalytics.trackDownload && Drupal.googleanalytics.isDownload(this.href)) {
+ // Download link clicked.
+ console.info("Download url '%s' has been found. Tracked download as extension '%s'.", Drupal.googleanalytics.getPageUrl(this.href), Drupal.googleanalytics.getDownloadExtension(this.href).toUpperCase());
+ ga("send", "event", "Downloads", Drupal.googleanalytics.getDownloadExtension(this.href).toUpperCase(), Drupal.googleanalytics.getPageUrl(this.href));
+ }
+ else if (Drupal.googleanalytics.isInternalSpecial(this.href)) {
+ // Keep the internal URL for Google Analytics website overlay intact.
+ console.info("Click on internal special link '%s' has been tracked.", Drupal.googleanalytics.getPageUrl(this.href));
+ ga("send", "pageview", { "page": Drupal.googleanalytics.getPageUrl(this.href) });
+ }
+ else {
+ // e.g. anchor in same page or other internal page link
+ console.info("Click on internal link '%s' detected, but not tracked by click.", this.href);
+ }
+ }
+ else {
+ if (Drupal.settings.googleanalytics.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
+ // Mailto link clicked.
+ console.info("Click on e-mail '%s' has been tracked.", this.href.substring(7));
+ ga("send", "event", "Mails", "Click", this.href.substring(7));
+ }
+ else if (Drupal.settings.googleanalytics.trackOutbound && this.href.match(/^\w+:\/\//i)) {
+ if (Drupal.settings.googleanalytics.trackDomainMode != 2 || (Drupal.settings.googleanalytics.trackDomainMode == 2 && !Drupal.googleanalytics.isCrossDomain(this.hostname, Drupal.settings.googleanalytics.trackCrossDomains))) {
+ // External link clicked / No top-level cross domain clicked.
+ console.info("Outbound link '%s' has been tracked.", this.href);
+ ga("send", "event", "Outbound links", "Click", this.href);
+ }
+ else {
+ console.info("Internal link '%s' clicked, not tracked.", this.href);
+ }
+ }
+ }
+ });
+
+ console.groupEnd();
+ });
+
+ // Track hash changes as unique pageviews, if this option has been enabled.
+ if (Drupal.settings.googleanalytics.trackUrlFragments) {
+ window.onhashchange = function() {
+ console.info("Track URL '%s' as pageview. Hash '%s' has changed.", location.pathname + location.search + location.hash, location.hash);
+ ga('send', 'pageview', location.pathname + location.search + location.hash);
+ }
+ }
+
+ // Colorbox: This event triggers when the transition has completed and the
+ // newly loaded content has been revealed.
+ $(document).bind("cbox_complete", function () {
+ var href = $.colorbox.element().attr("href");
+ if (href) {
+ console.info("Colorbox transition to url '%s' has been tracked.", Drupal.googleanalytics.getPageUrl(href));
+ ga("send", "pageview", { "page": Drupal.googleanalytics.getPageUrl(href) });
+ }
+ });
+
+});
+
+/**
+ * Check whether the hostname is part of the cross domains or not.
+ *
+ * @param string hostname
+ * The hostname of the clicked URL.
+ * @param array crossDomains
+ * All cross domain hostnames as JS array.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isCrossDomain = function (hostname, crossDomains) {
+ /**
+ * jQuery < 1.6.3 bug: $.inArray crushes IE6 and Chrome if second argument is
+ * `null` or `undefined`, http://bugs.jquery.com/ticket/10076,
+ * https://github.com/jquery/jquery/commit/a839af034db2bd934e4d4fa6758a3fed8de74174
+ *
+ * @todo: Remove/Refactor in D8
+ */
+ if (!crossDomains) {
+ return false;
+ }
+ else {
+ return $.inArray(hostname, crossDomains) > -1 ? true : false;
+ }
+};
+
+/**
+ * Check whether this is a download URL or not.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isDownload = function (url) {
+ var isDownload = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+ return isDownload.test(url);
+};
+
+/**
+ * Check whether this is an absolute internal URL or not.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternal = function (url) {
+ var isInternal = new RegExp("^(https?):\/\/" + window.location.host, "i");
+ return isInternal.test(url);
+};
+
+/**
+ * Check whether this is a special URL or not.
+ *
+ * URL types:
+ * - gotwo.module /go/* links.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternalSpecial = function (url) {
+ var isInternalSpecial = new RegExp("(\/go\/.*)$", "i");
+ return isInternalSpecial.test(url);
+};
+
+/**
+ * Extract the relative internal URL from an absolute internal URL.
+ *
+ * Examples:
+ * - http://mydomain.com/node/1 -> /node/1
+ * - http://example.com/foo/bar -> http://example.com/foo/bar
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return string
+ * Internal website URL
+ */
+Drupal.googleanalytics.getPageUrl = function (url) {
+ var extractInternalUrl = new RegExp("^(https?):\/\/" + window.location.host, "i");
+ return url.replace(extractInternalUrl, '');
+};
+
+/**
+ * Extract the download file extension from the URL.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return string
+ * The file extension of the passed url. e.g. "zip", "txt"
+ */
+Drupal.googleanalytics.getDownloadExtension = function (url) {
+ var extractDownloadextension = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+ var extension = extractDownloadextension.exec(url);
+ return (extension === null) ? '' : extension[1];
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.info b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.info
index fea44ca3..adbf134a 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.info
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.info
@@ -4,10 +4,10 @@ core = 7.x
package = Statistics
configure = admin/config/system/googleanalytics
files[] = googleanalytics.test
-
-; Information added by drupal.org packaging script on 2012-11-01
-version = "7.x-1.3"
+test_dependencies[] = token
+; Information added by Drupal.org packaging script on 2014-11-29
+version = "7.x-2.1"
core = "7.x"
project = "google_analytics"
-datestamp = "1351810914"
+datestamp = "1417276982"
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.install b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.install
index b3841cb8..d7b2edfb 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.install
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.install
@@ -5,33 +5,21 @@
* Installation file for Google Analytics module.
*/
-/**
- * Implements hook_install().
- */
-function googleanalytics_install() {
- // By German laws it's always best to enable the anonymizing of IP addresses.
- // NOTE: If this is also an important default setting in other countries, please let us know!
- $countries = array(
- 'DE',
- );
- if (in_array(variable_get('site_default_country', ''), $countries)) {
- variable_set('googleanalytics_tracker_anonymizeip', 1);
- }
-}
-
/**
* Implements hook_uninstall().
*/
function googleanalytics_uninstall() {
variable_del('googleanalytics_account');
variable_del('googleanalytics_cache');
+ variable_del('googleanalytics_codesnippet_create');
variable_del('googleanalytics_codesnippet_before');
variable_del('googleanalytics_codesnippet_after');
variable_del('googleanalytics_cross_domains');
variable_del('googleanalytics_custom');
- variable_del('googleanalytics_custom_var');
+ variable_del('googleanalytics_custom_dimension');
+ variable_del('googleanalytics_custom_metric');
+ variable_del('googleanalytics_debug');
variable_del('googleanalytics_domain_mode');
- variable_del('googleanalytics_js_scope');
variable_del('googleanalytics_last_cache');
variable_del('googleanalytics_pages');
variable_del('googleanalytics_roles');
@@ -41,16 +29,20 @@ function googleanalytics_uninstall() {
variable_del('googleanalytics_tracker_anonymizeip');
variable_del('googleanalytics_trackfiles');
variable_del('googleanalytics_trackfiles_extensions');
+ variable_del('googleanalytics_tracklinkid');
+ variable_del('googleanalytics_trackurlfragments');
+ variable_del('googleanalytics_trackuserid');
variable_del('googleanalytics_trackmailto');
+ variable_del('googleanalytics_trackmessages');
variable_del('googleanalytics_trackoutbound');
variable_del('googleanalytics_translation_set');
variable_del('googleanalytics_visibility_pages');
variable_del('googleanalytics_visibility_roles');
+ variable_del('googleanalytics_privacy_donottrack');
// Remove backup variables if exist. Remove this code in D8.
- variable_del('googleanalytics_codesnippet_after_backup_6300');
- variable_del('googleanalytics_codesnippet_before_backup_6300');
- variable_del('googleanalytics_segmentation');
+ variable_del('googleanalytics_codesnippet_after_backup_7200');
+ variable_del('googleanalytics_codesnippet_before_backup_7200');
}
/**
@@ -71,14 +63,24 @@ function googleanalytics_requirements($phase) {
if ($phase == 'runtime') {
// Raise warning if Google user account has not been set yet.
- if (!preg_match('/^UA-\d{4,}-\d+$/', variable_get('googleanalytics_account', 'UA-'))) {
- $requirements['googleanalytics'] = array(
+ if (!preg_match('/^UA-\d+-\d+$/', variable_get('googleanalytics_account', 'UA-'))) {
+ $requirements['googleanalytics_account'] = array(
'title' => $t('Google Analytics module'),
'description' => $t('Google Analytics module has not been configured yet. Please configure its settings from the Google Analytics settings page.', array('@url' => url('admin/config/system/googleanalytics'))),
'severity' => REQUIREMENT_WARNING,
'value' => $t('Not configured'),
);
}
+
+ // Raise warning if debugging is enabled.
+ if (variable_get('googleanalytics_debug', 0)) {
+ $requirements['google_analytics_debugging'] = array(
+ 'title' => $t('Google Analytics module'),
+ 'description' => $t('Google Analytics module has debugging enabled. Please disable debugging setting in production sites from the Google Analytics settings page.', array('@url' => url('admin/config/system/googleanalytics'))),
+ 'severity' => REQUIREMENT_WARNING,
+ 'value' => $t('Debugging enabled'),
+ );
+ }
}
return $requirements;
@@ -425,10 +427,111 @@ function googleanalytics_update_7006() {
}
/**
-* Delete obsolete googleanalytics_trackpageloadtime variable.
-*/
+ * Delete obsolete googleanalytics_trackpageloadtime variable.
+ */
function googleanalytics_update_7007() {
variable_del('googleanalytics_trackpageloadtime');
return t('Deleted obsolete googleanalytics_trackpageloadtime variable.');
}
+
+/**
+ * Delete custom ga.js code snipptes to prevent malfunctions in new Universal Analytics tracker. A backup of your snippets will be created.
+ */
+function googleanalytics_update_7200() {
+ $messages = array();
+
+ // ga.js code will cause the tracker to break. Remove custom code snippets.
+ $googleanalytics_codesnippet_before = variable_get('googleanalytics_codesnippet_before', '');
+ if (!empty($googleanalytics_codesnippet_before) && stristr($googleanalytics_codesnippet_before, '_gaq.push(')) {
+ variable_set('googleanalytics_codesnippet_before_backup_7200', $googleanalytics_codesnippet_before);
+ variable_del('googleanalytics_codesnippet_before');
+ drupal_set_message(Database::getConnection()->prefixTables("A backup of your previous Google Analytics code snippet has been saved in database table '{variable}' as 'googleanalytics_codesnippet_before_backup_7200'. You need to manually upgrade the custom 'before' code snippet."), 'warning');
+ $messages[] = t('Manual upgrade of custom "before" code snippet is required.');
+ }
+
+ $googleanalytics_codesnippet_after = variable_get('googleanalytics_codesnippet_after', '');
+ if (!empty($googleanalytics_codesnippet_after) && stristr($googleanalytics_codesnippet_after, '_gaq.push(')) {
+ variable_set('googleanalytics_codesnippet_after_backup_7200', $googleanalytics_codesnippet_after);
+ variable_del('googleanalytics_codesnippet_after');
+ drupal_set_message(Database::getConnection()->prefixTables("A backup of your previous Google Analytics code snippet has been saved in database table '{variable}' as 'googleanalytics_codesnippet_before_backup_7200'. You need to manually upgrade the custom 'before' code snippet."), 'warning');
+ $messages[] = t('Manual upgrade of custom "after" code snippet is required.');
+ }
+
+ return empty($messages) ? t('No custom code snipped found. Nothing to do.') : implode(' ', $messages);
+}
+
+/**
+ * Delete obsolete custom variables. Custom variables are now custom dimensions and metrics.
+ */
+function googleanalytics_update_7201() {
+ variable_del('googleanalytics_custom_var');
+
+ return t('Deleted obsolete custom variables. Custom variables are now custom dimensions and metrics and you need to manually configure them!');
+}
+
+/**
+ * Delete obsolete JavaScript scope variable.
+ */
+function googleanalytics_update_7202() {
+ // Remove obsolete scope variable
+ variable_del('googleanalytics_js_scope');
+
+ return t('Removed obsolete JavaScript scope variable.');
+}
+
+/**
+ * Flatten the metrics and dimensions arrays.
+ */
+function googleanalytics_update_7203() {
+ $googleanalytics_custom_dimension = variable_get('googleanalytics_custom_dimension', array());
+ if (isset($googleanalytics_custom_dimension['indexes'])) {
+ foreach ($googleanalytics_custom_dimension['indexes'] as $dimension) {
+ $googleanalytics_custom_dimension['indexes'][$dimension['index']]['value'] = trim($dimension['value']);
+ // Remove empty values from the array.
+ if (!drupal_strlen($googleanalytics_custom_dimension['indexes'][$dimension['index']]['value'])) {
+ unset($googleanalytics_custom_dimension['indexes'][$dimension['index']]);
+ }
+ }
+ variable_set('googleanalytics_custom_dimension', $googleanalytics_custom_dimension['indexes']);
+ }
+ $googleanalytics_custom_metric = variable_get('googleanalytics_custom_metric', array());
+ if (isset($googleanalytics_custom_metric['indexes'])) {
+ foreach ($googleanalytics_custom_metric['indexes'] as $dimension) {
+ $googleanalytics_custom_metric['indexes'][$dimension['index']]['value'] = trim($dimension['value']);
+ // Remove empty values from the array.
+ if (!drupal_strlen($googleanalytics_custom_metric['indexes'][$dimension['index']]['value'])) {
+ unset($googleanalytics_custom_metric['indexes'][$dimension['index']]);
+ }
+ }
+ variable_set('googleanalytics_custom_metric', $googleanalytics_custom_metric['indexes']);
+ }
+
+ return t('Saved custom dimensions and metrics.');
+}
+
+/**
+ * Remove obsolete backup variables.
+ */
+function googleanalytics_update_7204() {
+ variable_del('googleanalytics_segmentation');
+ variable_del('googleanalytics_codesnippet_after_backup_6300');
+ variable_del('googleanalytics_codesnippet_before_backup_6300');
+ variable_del('googleanalytics_codesnippet_after_backup_6400');
+ variable_del('googleanalytics_codesnippet_before_backup_6400');
+
+ return t('Removed obsolete backup variables.');
+}
+
+/**
+ * Update list of default file extensions.
+ */
+function googleanalytics_update_7205() {
+ if (variable_get('googleanalytics_trackfiles_extensions', '') == '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls|xml|z|zip') {
+ variable_set('googleanalytics_trackfiles_extensions', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip');
+ return t('The default extensions for download tracking have been updated.');
+ }
+ else {
+ return t('Custom extensions for download tracking setting found. Update skipped!');
+ }
+}
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.js b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.js
index 58be03d2..1eed3c32 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.js
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.js
@@ -1,64 +1,61 @@
(function ($) {
+Drupal.googleanalytics = {};
+
$(document).ready(function() {
- // Expression to check for absolute internal links.
- var isInternal = new RegExp("^(https?):\/\/" + window.location.host, "i");
+ // Attach mousedown, keyup, touchstart events to document only and catch
+ // clicks on all elements.
+ $(document.body).bind("mousedown keyup touchstart", function(event) {
- // Attach onclick event to document only and catch clicks on all elements.
- $(document.body).click(function(event) {
// Catch the closest surrounding link of a clicked element.
$(event.target).closest("a,area").each(function() {
- var ga = Drupal.settings.googleanalytics;
- // Expression to check for special links like gotwo.module /go/* links.
- var isInternalSpecial = new RegExp("(\/go\/.*)$", "i");
- // Expression to check for download links.
- var isDownload = new RegExp("\\.(" + ga.trackDownloadExtensions + ")$", "i");
-
// Is the clicked URL internal?
- if (isInternal.test(this.href)) {
+ if (Drupal.googleanalytics.isInternal(this.href)) {
// Skip 'click' tracking, if custom tracking events are bound.
if ($(this).is('.colorbox')) {
// Do nothing here. The custom event will handle all tracking.
+ //console.info("Click on .colorbox item has been detected.");
}
// Is download tracking activated and the file extension configured for download tracking?
- else if (ga.trackDownload && isDownload.test(this.href)) {
+ else if (Drupal.settings.googleanalytics.trackDownload && Drupal.googleanalytics.isDownload(this.href)) {
// Download link clicked.
- var extension = isDownload.exec(this.href);
- _gaq.push(["_trackEvent", "Downloads", extension[1].toUpperCase(), this.href.replace(isInternal, '')]);
+ ga("send", "event", "Downloads", Drupal.googleanalytics.getDownloadExtension(this.href).toUpperCase(), Drupal.googleanalytics.getPageUrl(this.href));
}
- else if (isInternalSpecial.test(this.href)) {
+ else if (Drupal.googleanalytics.isInternalSpecial(this.href)) {
// Keep the internal URL for Google Analytics website overlay intact.
- _gaq.push(["_trackPageview", this.href.replace(isInternal, '')]);
+ ga("send", "pageview", { "page": Drupal.googleanalytics.getPageUrl(this.href) });
}
}
else {
- if (ga.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
+ if (Drupal.settings.googleanalytics.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
// Mailto link clicked.
- _gaq.push(["_trackEvent", "Mails", "Click", this.href.substring(7)]);
+ ga("send", "event", "Mails", "Click", this.href.substring(7));
}
- else if (ga.trackOutbound && this.href.match(/^\w+:\/\//i)) {
- if (ga.trackDomainMode == 2 && isCrossDomain($(this).attr('hostname'), ga.trackCrossDomains)) {
- // Top-level cross domain clicked. document.location is handled by _link internally.
- event.preventDefault();
- _gaq.push(["_link", this.href]);
- }
- else {
- // External link clicked.
- _gaq.push(["_trackEvent", "Outbound links", "Click", this.href]);
+ else if (Drupal.settings.googleanalytics.trackOutbound && this.href.match(/^\w+:\/\//i)) {
+ if (Drupal.settings.googleanalytics.trackDomainMode != 2 || (Drupal.settings.googleanalytics.trackDomainMode == 2 && !Drupal.googleanalytics.isCrossDomain(this.hostname, Drupal.settings.googleanalytics.trackCrossDomains))) {
+ // External link clicked / No top-level cross domain clicked.
+ ga("send", "event", "Outbound links", "Click", this.href);
}
}
}
});
});
+ // Track hash changes as unique pageviews, if this option has been enabled.
+ if (Drupal.settings.googleanalytics.trackUrlFragments) {
+ window.onhashchange = function() {
+ ga('send', 'pageview', location.pathname + location.search + location.hash);
+ }
+ }
+
// Colorbox: This event triggers when the transition has completed and the
// newly loaded content has been revealed.
- $(document).bind("cbox_complete", function() {
+ $(document).bind("cbox_complete", function () {
var href = $.colorbox.element().attr("href");
if (href) {
- _gaq.push(["_trackPageview", href.replace(isInternal, '')]);
+ ga("send", "pageview", { "page": Drupal.googleanalytics.getPageUrl(href) });
}
});
@@ -74,7 +71,7 @@ $(document).ready(function() {
*
* @return boolean
*/
-function isCrossDomain(hostname, crossDomains) {
+Drupal.googleanalytics.isCrossDomain = function (hostname, crossDomains) {
/**
* jQuery < 1.6.3 bug: $.inArray crushes IE6 and Chrome if second argument is
* `null` or `undefined`, http://bugs.jquery.com/ticket/10076,
@@ -88,6 +85,81 @@ function isCrossDomain(hostname, crossDomains) {
else {
return $.inArray(hostname, crossDomains) > -1 ? true : false;
}
-}
+};
+
+/**
+ * Check whether this is a download URL or not.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isDownload = function (url) {
+ var isDownload = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+ return isDownload.test(url);
+};
+
+/**
+ * Check whether this is an absolute internal URL or not.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternal = function (url) {
+ var isInternal = new RegExp("^(https?):\/\/" + window.location.host, "i");
+ return isInternal.test(url);
+};
+
+/**
+ * Check whether this is a special URL or not.
+ *
+ * URL types:
+ * - gotwo.module /go/* links.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternalSpecial = function (url) {
+ var isInternalSpecial = new RegExp("(\/go\/.*)$", "i");
+ return isInternalSpecial.test(url);
+};
+
+/**
+ * Extract the relative internal URL from an absolute internal URL.
+ *
+ * Examples:
+ * - http://mydomain.com/node/1 -> /node/1
+ * - http://example.com/foo/bar -> http://example.com/foo/bar
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return string
+ * Internal website URL
+ */
+Drupal.googleanalytics.getPageUrl = function (url) {
+ var extractInternalUrl = new RegExp("^(https?):\/\/" + window.location.host, "i");
+ return url.replace(extractInternalUrl, '');
+};
+
+/**
+ * Extract the download file extension from the URL.
+ *
+ * @param string url
+ * The web url to check.
+ *
+ * @return string
+ * The file extension of the passed url. e.g. "zip", "txt"
+ */
+Drupal.googleanalytics.getDownloadExtension = function (url) {
+ var extractDownloadextension = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+ var extension = extractDownloadextension.exec(url);
+ return (extension === null) ? '' : extension[1];
+};
})(jQuery);
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.module b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.module
index 9efae875..4051b1b9 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.module
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.module
@@ -1,19 +1,35 @@
*/
-define('GOOGLEANALYTICS_TRACKFILES_EXTENSIONS', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls|xml|z|zip');
+/**
+ * Define the default file extension list that should be tracked as download.
+ */
+define('GOOGLEANALYTICS_TRACKFILES_EXTENSIONS', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip');
-// Remove tracking from all administrative pages, see http://drupal.org/node/34970.
+/**
+ * Define default path exclusion list to remove tracking from admin pages,
+ * see http://drupal.org/node/34970 for more information.
+ */
define('GOOGLEANALYTICS_PAGES', "admin\nadmin/*\nbatch\nnode/add*\nnode/*/*\nuser/*/*");
+/**
+ * Advertise the supported google analytics api details.
+ */
+function googleanalytics_api() {
+ return array(
+ 'api' => 'analytics.js',
+ );
+}
+
/**
* Implements hook_help().
*/
@@ -88,19 +104,14 @@ function googleanalytics_page_alter(&$page) {
'404 Not Found',
);
- // 1. Check if the GA account number has a value.
+ // 1. Check if the GA account number has a valid value.
// 2. Track page views based on visibility value.
// 3. Check if we should track the currently active user's role.
// 4. Ignore pages visibility filter for 404 or 403 status codes.
- if (!empty($id) && (_googleanalytics_visibility_pages() || in_array($status, $trackable_status_codes)) && _googleanalytics_visibility_user($user)) {
+ if (preg_match('/^UA-\d+-\d+$/', $id) && (_googleanalytics_visibility_pages() || in_array($status, $trackable_status_codes)) && _googleanalytics_visibility_user($user)) {
- // We allow different scopes. Default to 'header' but allow user to override if they really need to.
- $scope = variable_get('googleanalytics_js_scope', 'header');
-
- if (variable_get('googleanalytics_trackadsense', FALSE)) {
- // Custom tracking. Prepend before all other JavaScript.
- drupal_add_js('window.google_analytics_uacct = ' . drupal_json_encode($id) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1));
- }
+ $debug = variable_get('googleanalytics_debug', 0);
+ $url_custom = '';
// Add link tracking.
$link_settings = array();
@@ -120,10 +131,23 @@ function googleanalytics_page_alter(&$page) {
if ($track_cross_domains = variable_get('googleanalytics_cross_domains', '')) {
$link_settings['trackCrossDomains'] = preg_split('/(\r\n?|\n)/', $track_cross_domains);
}
+ if ($track_url_fragments = variable_get('googleanalytics_trackurlfragments', 0)) {
+ $link_settings['trackUrlFragments'] = $track_url_fragments;
+ $url_custom = 'location.pathname + location.search + location.hash';
+ }
if (!empty($link_settings)) {
drupal_add_js(array('googleanalytics' => $link_settings), 'setting');
- drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.js');
+
+ // Add debugging code.
+ if ($debug) {
+ drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.debug.js');
+ // Add the JS test in development to the page.
+ //drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.test.js');
+ }
+ else {
+ drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.js');
+ }
}
// Add messages tracking.
@@ -140,15 +164,17 @@ function googleanalytics_page_alter(&$page) {
// Track only the selected message types.
if (in_array($type, $message_types)) {
foreach ($messages as $message) {
- $message_events .= '_gaq.push(["_trackEvent", ' . drupal_json_encode(t('Messages')) . ', ' . drupal_json_encode($status_heading[$type]) . ', ' . drupal_json_encode(strip_tags($message)) . ']);';
+ // @todo: Track as exceptions?
+ $message_events .= 'ga("send", "event", ' . drupal_json_encode(t('Messages')) . ', ' . drupal_json_encode($status_heading[$type]) . ', ' . drupal_json_encode(strip_tags($message)) . ');';
}
}
}
}
// Site search tracking support.
- $url_custom = '';
if (module_exists('search') && variable_get('googleanalytics_site_search', FALSE) && arg(0) == 'search' && $keys = googleanalytics_search_get_keys()) {
+ // hook_preprocess_search_results() is not executed if search result is
+ // empty. Make sure the counter is set to 0 if there are no results.
$url_custom = '(window.googleanalytics_search_results) ? ' . drupal_json_encode(url('search/' . arg(1), array('query' => array('search' => $keys)))) . ' : ' . drupal_json_encode(url('search/' . arg(1), array('query' => array('search' => 'no-results:' . $keys, 'cat' => 'no-results'))));
}
@@ -174,70 +200,134 @@ function googleanalytics_page_alter(&$page) {
$url_custom = '"/404.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
}
- // Add any custom code snippets if specified.
- $codesnippet_before = variable_get('googleanalytics_codesnippet_before', '');
- $codesnippet_after = variable_get('googleanalytics_codesnippet_after', '');
-
- // Add custom variables.
- $googleanalytics_custom_vars = variable_get('googleanalytics_custom_var', array());
+ // Add custom dimensions and metrics.
$custom_var = '';
- for ($i = 1; $i < 6; $i++) {
- $custom_var_name = !empty($googleanalytics_custom_vars['slots'][$i]['name']) ? $googleanalytics_custom_vars['slots'][$i]['name'] : '';
- if (!empty($custom_var_name)) {
- $custom_var_value = !empty($googleanalytics_custom_vars['slots'][$i]['value']) ? $googleanalytics_custom_vars['slots'][$i]['value'] : '';
- $custom_var_scope = !empty($googleanalytics_custom_vars['slots'][$i]['scope']) ? $googleanalytics_custom_vars['slots'][$i]['scope'] : 3;
+ foreach (array('dimension', 'metric') as $googleanalytics_custom_type) {
+ $googleanalytics_custom_vars = variable_get('googleanalytics_custom_' . $googleanalytics_custom_type, array());
+ // Are there dimensions or metrics configured?
+ if (!empty($googleanalytics_custom_vars)) {
+ // Add all the configured variables to the content.
+ foreach ($googleanalytics_custom_vars as $googleanalytics_custom_var) {
+ // Replace tokens in values.
+ $types = array();
+ $node = menu_get_object();
+ if (is_object($node)) {
+ $types += array('node' => $node);
+ }
+ $googleanalytics_custom_var['value'] = token_replace($googleanalytics_custom_var['value'], $types, array('clear' => TRUE));
- $types = array();
- $node = menu_get_object();
- if (is_object($node)) {
- $types += array('node' => $node);
+ // Suppress empty values.
+ if (!drupal_strlen(trim($googleanalytics_custom_var['value']))) {
+ continue;
+ }
+
+ // Per documentation the max length of a dimension is 150 bytes.
+ // A metric has no length limitation. It's not documented if this
+ // limit means 150 bytes after url encoding or before.
+ // See https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs
+ if ($googleanalytics_custom_type == 'dimension' && drupal_strlen($googleanalytics_custom_var['value']) > 150) {
+ $googleanalytics_custom_var['value'] = substr($googleanalytics_custom_var['value'], 0, 150);
+ }
+
+ // Cast metric values for json_encode to data type numeric.
+ if ($googleanalytics_custom_type == 'metric') {
+ settype($googleanalytics_custom_var['value'], 'float');
+ };
+
+ // Add variables to tracker.
+ $custom_var .= 'ga("set", ' . drupal_json_encode($googleanalytics_custom_type . $googleanalytics_custom_var['index']) . ', ' . drupal_json_encode($googleanalytics_custom_var['value']) . ');';
}
- $custom_var_name = token_replace($custom_var_name, $types, array('clear' => TRUE));
- $custom_var_value = token_replace($custom_var_value, $types, array('clear' => TRUE));
-
- // Suppress empty custom names and/or variables.
- if (!drupal_strlen(trim($custom_var_name)) || !drupal_strlen(trim($custom_var_value))) {
- continue;
- }
-
- // The length of the string used for the 'name' and the length of the
- // string used for the 'value' must not exceed 128 bytes after url encoding.
- $name_length = drupal_strlen(rawurlencode($custom_var_name));
- $tmp_value = rawurlencode($custom_var_value);
- $value_length = drupal_strlen($tmp_value);
- if ($name_length + $value_length > 128) {
- // Trim value and remove fragments of url encoding.
- $tmp_value = rtrim(substr($tmp_value, 0, 127 - $name_length), '%0..9A..F');
- $custom_var_value = urldecode($tmp_value);
- }
-
- $custom_var_name = drupal_json_encode($custom_var_name);
- $custom_var_value = drupal_json_encode($custom_var_value);
- $custom_var .= "_gaq.push(['_setCustomVar', $i, $custom_var_name, $custom_var_value, $custom_var_scope]);";
}
}
// Build tracker code.
- $script = 'var _gaq = _gaq || [];';
- $script .= '_gaq.push(["_setAccount", ' . drupal_json_encode($id) . ']);';
- if (variable_get('googleanalytics_tracker_anonymizeip', 0)) {
- // FIXME: The Google API is currently broken and "_gat._anonymizeIp" is only
- // a workaround until "_anonymizeIp" has been implemented/fixed.
- $script .= '_gaq.push(["_gat._anonymizeIp"]);';
+ $script = '(function(i,s,o,g,r,a,m){';
+ $script .= 'i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){';
+ $script .= '(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),';
+ $script .= 'm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)';
+ $script .= '})(window,document,"script",';
+
+ // Which version of the tracking library should be used?
+ $library_tracker_url = '//www.google-analytics.com/' . ($debug ? 'analytics_debug.js' : 'analytics.js');
+ $library_cache_url = 'http:' . $library_tracker_url;
+
+ // Should a local cached copy of analytics.js be used?
+ if (variable_get('googleanalytics_cache', 0) && $url = _googleanalytics_cache($library_cache_url)) {
+ // A dummy query-string is added to filenames, to gain control over
+ // browser-caching. The string changes on every update or full cache
+ // flush, forcing browsers to load a new copy of the files, as the
+ // URL changed.
+ $query_string = '?' . variable_get('css_js_query_string', '0');
+
+ $script .= '"' . $url . $query_string . '"';
}
+ else {
+ $script .= '"' . $library_tracker_url . '"';
+ }
+ $script .= ',"ga");';
+
+ // Add any custom code snippets if specified.
+ $codesnippet_create = variable_get('googleanalytics_codesnippet_create', array());
+ $codesnippet_before = variable_get('googleanalytics_codesnippet_before', '');
+ $codesnippet_after = variable_get('googleanalytics_codesnippet_after', '');
+
+ // Build the create only fields list.
+ $create_only_fields = array('cookieDomain' => 'auto');
+ $create_only_fields = array_merge($create_only_fields, $codesnippet_create);
// Domain tracking type.
global $cookie_domain;
$domain_mode = variable_get('googleanalytics_domain_mode', 0);
+ $googleanalytics_adsense_script = '';
// Per RFC 2109, cookie domains must contain at least one dot other than the
// first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
- $script .= '_gaq.push(["_setDomainName", ' . drupal_json_encode($cookie_domain) . ']);';
+ $create_only_fields = array_merge($create_only_fields, array('cookieDomain' => $cookie_domain));
+ $googleanalytics_adsense_script .= 'window.google_analytics_domain_name = ' . drupal_json_encode($cookie_domain) . ';';
}
elseif ($domain_mode == 2) {
- $script .= '_gaq.push(["_setDomainName", "none"]);';
- $script .= '_gaq.push(["_setAllowLinker", true]);';
+ // Cross Domain tracking. 'autoLinker' need to be enabled in 'create'.
+ $create_only_fields = array_merge($create_only_fields, array('allowLinker' => TRUE));
+ $googleanalytics_adsense_script .= 'window.google_analytics_domain_name = "none";';
+ }
+
+ // Track logged in users across all devices.
+ if (variable_get('googleanalytics_trackuserid', 0) && user_is_logged_in()) {
+ // The USER_ID value should be a unique, persistent, and non-personally
+ // identifiable string identifier that represents a user or signed-in
+ // account across devices.
+ $create_only_fields['userId'] = drupal_hmac_base64($user->uid, drupal_get_private_key() . drupal_get_hash_salt());
+ }
+
+ // Create a tracker.
+ $script .= 'ga("create", ' . drupal_json_encode($id) . ', ' . drupal_json_encode($create_only_fields) .');';
+
+ // Prepare Adsense tracking.
+ $googleanalytics_adsense_script .= 'window.google_analytics_uacct = ' . drupal_json_encode($id) . ';';
+
+ // Add enhanced link attribution after 'create', but before 'pageview' send.
+ // @see https://support.google.com/analytics/answer/2558867
+ if (variable_get('googleanalytics_tracklinkid', 0)) {
+ $script .= 'ga("require", "linkid", "linkid.js");';
+ }
+
+ // Add display features after 'create', but before 'pageview' send.
+ // @see https://support.google.com/analytics/answer/2444872
+ if (variable_get('googleanalytics_trackdoubleclick', 0)) {
+ $script .= 'ga("require", "displayfeatures");';
+ }
+
+ // Domain tracking type.
+ if ($domain_mode == 2) {
+ // Cross Domain tracking
+ // https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#cross-domain
+ $script .= 'ga("require", "linker");';
+ $script .= 'ga("linker:autoLink", ' . drupal_json_encode($link_settings['trackCrossDomains']) . ');';
+ }
+
+ if (variable_get('googleanalytics_tracker_anonymizeip', 1)) {
+ $script .= 'ga("set", "anonymizeIp", true);';
}
if (!empty($custom_var)) {
@@ -246,12 +336,11 @@ function googleanalytics_page_alter(&$page) {
if (!empty($codesnippet_before)) {
$script .= $codesnippet_before;
}
- if (empty($url_custom)) {
- $script .= '_gaq.push(["_trackPageview"]);';
- }
- else {
- $script .= '_gaq.push(["_trackPageview", ' . $url_custom . ']);';
+ if (!empty($url_custom)) {
+ $script .= 'ga("set", "page", ' . $url_custom . ');';
}
+ $script .= 'ga("send", "pageview");';
+
if (!empty($message_events)) {
$script .= $message_events;
}
@@ -259,45 +348,14 @@ function googleanalytics_page_alter(&$page) {
$script .= $codesnippet_after;
}
- $script .= '(function() {';
- $script .= 'var ga = document.createElement("script");';
- $script .= 'ga.type = "text/javascript";';
- $script .= 'ga.async = true;';
-
- // Which version of the tracking library should be used?
- if ($trackdoubleclick = variable_get('googleanalytics_trackdoubleclick', FALSE)) {
- $library_tracker_url = 'stats.g.doubleclick.net/dc.js';
- $library_cache_url = 'http://' . $library_tracker_url;
- }
- else {
- $library_tracker_url = '.google-analytics.com/ga.js';
- $library_cache_url = 'http://www' . $library_tracker_url;
+ if (variable_get('googleanalytics_trackadsense', FALSE)) {
+ // Custom tracking. Prepend before all other JavaScript.
+ // @TODO: https://support.google.com/adsense/answer/98142
+ // sounds like it could be appended to $script.
+ drupal_add_js($googleanalytics_adsense_script, array('type' => 'inline', 'group' => JS_LIBRARY-1));
}
- // Should a local cached copy of ga.js be used?
- if (variable_get('googleanalytics_cache', 0) && $url = _googleanalytics_cache($library_cache_url)) {
- // A dummy query-string is added to filenames, to gain control over
- // browser-caching. The string changes on every update or full cache
- // flush, forcing browsers to load a new copy of the files, as the
- // URL changed.
- $query_string = '?' . variable_get('css_js_query_string', '0');
-
- $script .= 'ga.src = "' . $url . $query_string . '";';
- }
- else {
- // Library paths do not follow the same naming convention.
- if ($trackdoubleclick) {
- $script .= 'ga.src = ("https:" == document.location.protocol ? "https://" : "http://") + "' . $library_tracker_url . '";';
- }
- else {
- $script .= 'ga.src = ("https:" == document.location.protocol ? "https://ssl" : "http://www") + "' . $library_tracker_url . '";';
- }
- }
- $script .= 'var s = document.getElementsByTagName("script")[0];';
- $script .= 's.parentNode.insertBefore(ga, s);';
- $script .= '})();';
-
- drupal_add_js($script, array('scope' => $scope, 'type' => 'inline'));
+ drupal_add_js($script, array('scope' => 'header', 'type' => 'inline'));
}
}
@@ -382,13 +440,7 @@ function googleanalytics_user_presave(&$edit, $account, $category) {
function googleanalytics_cron() {
// Regenerate the tracking code file every day.
if (REQUEST_TIME - variable_get('googleanalytics_last_cache', 0) >= 86400 && variable_get('googleanalytics_cache', 0)) {
- // Which version of the tracking library should be used?
- if (variable_get('googleanalytics_trackdoubleclick', FALSE)) {
- _googleanalytics_cache('http://stats.g.doubleclick.net/dc.js', TRUE);
- }
- else {
- _googleanalytics_cache('http://www.google-analytics.com/ga.js', TRUE);
- }
+ _googleanalytics_cache('http://www.google-analytics.com/analytics.js', TRUE);
variable_set('googleanalytics_last_cache', REQUEST_TIME);
}
}
@@ -399,11 +451,13 @@ function googleanalytics_cron() {
* Collects and adds the number of search results to the head.
*/
function googleanalytics_preprocess_search_results(&$variables) {
- // There is no search result $variable available that hold the number of items
- // found. But the pager item mumber can tell the number of search results.
- global $pager_total_items;
+ if (variable_get('googleanalytics_site_search', FALSE)) {
+ // There is no search result $variable available that hold the number of items
+ // found. But the pager item mumber can tell the number of search results.
+ global $pager_total_items;
- drupal_add_js('window.googleanalytics_search_results = ' . intval($pager_total_items[0]) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1));
+ drupal_add_js('window.googleanalytics_search_results = ' . intval($pager_total_items[0]) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1));
+ }
}
/**
@@ -428,16 +482,16 @@ function googleanalytics_search_get_keys() {
*
* @param $location
* The full URL to the external javascript file.
- * @param $sync_cached_file
- * Synchronize tracking code and update if remote file have changed.
+ * @param $synchronize
+ * Synchronize to local cache if remote file has changed.
* @return mixed
* The path to the local javascript file on success, boolean FALSE on failure.
*/
-function _googleanalytics_cache($location, $sync_cached_file = FALSE) {
+function _googleanalytics_cache($location, $synchronize = FALSE) {
$path = 'public://googleanalytics';
$file_destination = $path . '/' . basename($location);
- if (!file_exists($file_destination) || $sync_cached_file) {
+ if (!file_exists($file_destination) || $synchronize) {
// Download the latest tracking code.
$result = drupal_http_request($location);
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test
index f8a68cd4..0b64bb82 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test
@@ -8,8 +8,8 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
public static function getInfo() {
return array(
- 'name' => t('Google Analytics basic tests'),
- 'description' => t('Test basic functionality of Google Analytics module.'),
+ 'name' => 'Google Analytics basic tests',
+ 'description' => 'Test basic functionality of Google Analytics module.',
'group' => 'Google Analytics',
);
}
@@ -20,6 +20,8 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
$permissions = array(
'access administration pages',
'administer google analytics',
+ 'administer modules',
+ 'administer site configuration',
);
// User to set up google_analytics.
@@ -28,23 +30,38 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
}
function testGoogleAnalyticsConfiguration() {
+ // Check if Configure link is available on 'Modules' page.
+ // Requires 'administer modules' permission.
+ $this->drupalGet('admin/modules');
+ $this->assertRaw('admin/config/system/googleanalytics', '[testGoogleAnalyticsConfiguration]: Configure link from Modules page to Google Analytics Settings page exists.');
+
+ // Check if Configure link is available on 'Status Reports' page. NOTE: Link is only shown without UA code configured.
+ // Requires 'administer site configuration' permission.
+ $this->drupalGet('admin/reports/status');
+ $this->assertRaw('admin/config/system/googleanalytics', '[testGoogleAnalyticsConfiguration]: Configure link from Status Reports page to Google Analytics Settings page exists.');
+
// Check for setting page's presence.
$this->drupalGet('admin/config/system/googleanalytics');
$this->assertRaw(t('Web Property ID'), '[testGoogleAnalyticsConfiguration]: Settings page displayed.');
// Check for account code validation.
$edit['googleanalytics_account'] = $this->randomName(2);
- $this->drupalPost('admin/config/system/googleanalytics', $edit, 'Save configuration');
+ $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration'));
$this->assertRaw(t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'), '[testGoogleAnalyticsConfiguration]: Invalid Web Property ID number validated.');
}
function testGoogleAnalyticsPageVisibility() {
+ // Verify that no tracking code is embedded into the webpage; if there is
+ // only the module installed, but UA code not configured. See #2246991.
+ $this->drupalGet('');
+ $this->assertNoRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed without UA code configured.');
+
$ua_code = 'UA-123456-1';
variable_set('googleanalytics_account', $ua_code);
// Show tracking on "every page except the listed pages".
variable_set('googleanalytics_visibility_pages', 0);
- // Disable tracking one "admin*" pages only.
+ // Disable tracking on "admin*" pages only.
variable_set('googleanalytics_pages', "admin\nadmin/*");
// Enable tracking only for authenticated users only.
variable_set('googleanalytics_roles', array(DRUPAL_AUTHENTICATED_RID => DRUPAL_AUTHENTICATED_RID));
@@ -58,7 +75,7 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
$this->assertNoRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin page.');
$this->drupalGet('admin/config/system/googleanalytics');
// Checking for tracking code URI here, as $ua_code is displayed in the form.
- $this->assertNoRaw('google-analytics.com/ga.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin subpage.');
+ $this->assertNoRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin subpage.');
// Test whether tracking code display is properly flipped.
variable_set('googleanalytics_visibility_pages', 1);
@@ -66,7 +83,7 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
$this->assertRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin page.');
$this->drupalGet('admin/config/system/googleanalytics');
// Checking for tracking code URI here, as $ua_code is displayed in the form.
- $this->assertRaw('google-analytics.com/ga.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin subpage.');
+ $this->assertRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin subpage.');
$this->drupalGet('');
$this->assertNoRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is NOT displayed on front page.');
@@ -89,22 +106,22 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
$this->assertRaw('/404.html', '[testGoogleAnalyticsPageVisibility]: 404 Not Found tracking code shown on non-existent page.');
// DNT Tests:
- // Enable caching of pages for anonymous users.
+ // Enable system internal page cache for anonymous users.
variable_set('cache', 1);
// Test whether DNT headers will fail to disable embedding of tracking code.
$this->drupalGet('', array(), array('DNT: 1'));
- $this->assertRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: DNT header send from client, but page caching is enabled and tracker cannot removed.');
- // DNT works only with caching of pages for anonymous users disabled.
+ $this->assertRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: DNT header send from client, but page caching is enabled and tracker cannot removed.');
+ // DNT works only with system internal page cache for anonymous users disabled.
variable_set('cache', 0);
$this->drupalGet('');
- $this->assertRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: Tracking is enabled without DNT header.');
+ $this->assertRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: Tracking is enabled without DNT header.');
// Test whether DNT header is able to remove the tracking code.
$this->drupalGet('', array(), array('DNT: 1'));
- $this->assertNoRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: DNT header received from client. Tracking has been disabled by browser.');
+ $this->assertNoRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: DNT header received from client. Tracking has been disabled by browser.');
// Disable DNT feature and see if tracker is still embedded.
variable_set('googleanalytics_privacy_donottrack', 0);
$this->drupalGet('', array(), array('DNT: 1'));
- $this->assertRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: DNT feature is disabled, DNT header from browser has been ignored.');
+ $this->assertRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: DNT feature is disabled, DNT header from browser has been ignored.');
}
function testGoogleAnalyticsTrackingCode() {
@@ -118,40 +135,75 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
/* Sample JS code as added to page:
-
+
*/
// Test whether tracking code uses latest JS.
variable_set('googleanalytics_cache', 0);
$this->drupalGet('');
- $this->assertRaw('google-analytics.com/ga.js', '[testGoogleAnalyticsTrackingCode]: Latest tracking code used.');
-
- // Test whether the alternate doubleclick library is used
- variable_set('googleanalytics_trackdoubleclick', 1);
- $this->drupalGet('');
- $this->assertRaw('stats.g.doubleclick.net/dc.js', '[testGoogleAnalyticsTrackingCode]: Doubleclick tracking code used.');
+ $this->assertRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsTrackingCode]: Latest tracking code used.');
// Test whether anonymize visitors IP address feature has been enabled.
+ variable_set('googleanalytics_tracker_anonymizeip', 0);
$this->drupalGet('');
- $this->assertNoRaw('_gaq.push(["_gat._anonymizeIp"]);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address not found on frontpage.');
+ $this->assertNoRaw('ga("set", "anonymizeIp", true);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address not found on frontpage.');
// Enable anonymizing of IP addresses.
variable_set('googleanalytics_tracker_anonymizeip', 1);
$this->drupalGet('');
- $this->assertRaw('_gaq.push(["_gat._anonymizeIp"]);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address found on frontpage.');
+ $this->assertRaw('ga("set", "anonymizeIp", true);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address found on frontpage.');
+
+ // Test if track Enhanced Link Attribution is enabled.
+ variable_set('googleanalytics_tracklinkid', 1);
+ $this->drupalGet('');
+ $this->assertRaw('ga("require", "linkid", "linkid.js");', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is enabled.');
+
+ // Test if track Enhanced Link Attribution is disabled.
+ variable_set('googleanalytics_tracklinkid', 0);
+ $this->drupalGet('');
+ $this->assertNoRaw('ga("require", "linkid", "linkid.js");', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is not enabled.');
+
+ // Test if tracking of User ID is enabled.
+ variable_set('googleanalytics_trackuserid', 1);
+ $this->drupalGet('');
+ $this->assertRaw(', {"cookieDomain":"auto","userId":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is enabled.');
+
+ // Test if tracking of User ID is disabled.
+ variable_set('googleanalytics_trackuserid', 0);
+ $this->drupalGet('');
+ $this->assertNoRaw(', {"cookieDomain":"auto","userId":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is disabled.');
+
+ // Test if tracking of url fragments is enabled.
+ variable_set('googleanalytics_trackurlfragments', 1);
+ $this->drupalGet('');
+ $this->assertRaw('ga("set", "page", location.pathname + location.search + location.hash);', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is enabled.');
+
+ // Test if tracking of url fragments is disabled.
+ variable_set('googleanalytics_trackurlfragments', 0);
+ $this->drupalGet('');
+ $this->assertNoRaw('ga("set", "page", location.pathname + location.search + location.hash);', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is not enabled.');
+
+ // Test if track display features is enabled.
+ variable_set('googleanalytics_trackdoubleclick', 1);
+ $this->drupalGet('');
+ $this->assertRaw('ga("require", "displayfeatures");', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is enabled.');
+
+ // Test if track display features is disabled.
+ variable_set('googleanalytics_trackdoubleclick', 0);
+ $this->drupalGet('');
+ $this->assertNoRaw('ga("require", "displayfeatures");', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is not enabled.');
// Test whether single domain tracking is active.
$this->drupalGet('');
- $this->assertNoRaw('_gaq.push(["_setDomainName"', '[testGoogleAnalyticsTrackingCode]: Single domain tracking is active.');
+ $this->assertRaw('{"cookieDomain":"auto"}', '[testGoogleAnalyticsTrackingCode]: Single domain tracking is active.');
// Enable "One domain with multiple subdomains".
variable_set('googleanalytics_domain_mode', 1);
@@ -161,36 +213,62 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
// TODO: Workaround to run tests successfully. This feature cannot tested reliable.
global $cookie_domain;
if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
- $this->assertRaw('_gaq.push(["_setDomainName",', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains is active on real host.');
+ $this->assertRaw('{"cookieDomain":"' . $cookie_domain . '"}', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains is active on real host.');
}
else {
// Special cases, Localhost and IP addresses don't show '_setDomainName'.
- $this->assertNoRaw('_gaq.push(["_setDomainName",', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
+ $this->assertNoRaw('{"cookieDomain":"' . $cookie_domain . '"}', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
}
// Enable "Multiple top-level domains" tracking.
variable_set('googleanalytics_domain_mode', 2);
variable_set('googleanalytics_cross_domains', "www.example.com\nwww.example.net");
$this->drupalGet('');
- $this->assertRaw('_gaq.push(["_setDomainName", "none"]);', '[testGoogleAnalyticsTrackingCode]: _setDomainName: "none" found. Cross domain tracking is active.');
- $this->assertRaw('_gaq.push(["_setAllowLinker", true]);', '[testGoogleAnalyticsTrackingCode]: _setAllowLinker: true found. Cross domain tracking is active.');
+ $this->assertRaw('ga("create", "' . $ua_code . '", {"cookieDomain":"auto","allowLinker":true', '[testGoogleAnalyticsTrackingCode]: "allowLinker" has been found. Cross domain tracking is active.');
+ $this->assertRaw('ga("require", "linker");', '[testGoogleAnalyticsTrackingCode]: Require linker has been found. Cross domain tracking is active.');
+ $this->assertRaw('ga("linker:autoLink", ["www.example.com","www.example.net"]);', '[testGoogleAnalyticsTrackingCode]: "linker:autoLink" has been found. Cross domain tracking is active.');
$this->assertRaw('"trackCrossDomains":["www.example.com","www.example.net"]', '[testGoogleAnalyticsTrackingCode]: Cross domain tracking with www.example.com and www.example.net is active.');
+ variable_set('googleanalytics_domain_mode', 0);
- // Test whether the BEFORE and AFTER code is added to the tracker.
- variable_set('googleanalytics_codesnippet_before', '_setDetectFlash(false);');
- variable_set('googleanalytics_codesnippet_after', '_gaq.push(["t2._setAccount", "UA-123456-3"]);_gaq.push(["t2._trackPageview"]);');
+ // Test whether debugging script has been enabled.
+ variable_set('googleanalytics_debug', 1);
$this->drupalGet('');
- $this->assertRaw('_setDetectFlash(false);', '[testGoogleAnalyticsTrackingCode]: Before codesnippet has been found with "Flash" detection disabled.');
- $this->assertRaw('t2._setAccount', '[testGoogleAnalyticsTrackingCode]: After codesnippet with "t2" tracker has been found.');
+ $this->assertRaw('//www.google-analytics.com/analytics_debug.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been enabled.');
+
+ // Check if text and link is shown on 'Status Reports' page.
+ // Requires 'administer site configuration' permission.
+ $this->drupalGet('admin/reports/status');
+ $this->assertRaw(t('Google Analytics module has debugging enabled. Please disable debugging setting in production sites from the Google Analytics settings page.', array('@url' => url('admin/config/system/googleanalytics'))), '[testGoogleAnalyticsConfiguration]: Debugging enabled is shown on Status Reports page.');
+
+ // Test whether debugging script has been disabled.
+ variable_set('googleanalytics_debug', 0);
+ $this->drupalGet('');
+ $this->assertRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been disabled.');
+
+ // Test whether the CREATE and BEFORE and AFTER code is added to the tracker.
+ $codesnippet_create = array(
+ 'cookieDomain' => 'foo.example.com',
+ 'cookieName' => 'myNewName',
+ 'cookieExpires' => 20000,
+ 'allowAnchor' => TRUE,
+ 'sampleRate' => 4.3,
+ );
+ variable_set('googleanalytics_codesnippet_create', $codesnippet_create);
+ variable_set('googleanalytics_codesnippet_before', 'ga("set", "forceSSL", true);');
+ variable_set('googleanalytics_codesnippet_after', 'ga("create", "UA-123456-3", {"name": "newTracker"});ga("newTracker.send", "pageview");');
+ $this->drupalGet('');
+ $this->assertRaw('ga("create", "' . $ua_code . '", {"cookieDomain":"foo.example.com","cookieName":"myNewName","cookieExpires":20000,"allowAnchor":true,"sampleRate":4.3});', '[testGoogleAnalyticsTrackingCode]: Create only fields have been found.');
+ $this->assertRaw('ga("set", "forceSSL", true);', '[testGoogleAnalyticsTrackingCode]: Before codesnippet will force http pages to also send all beacons using https.');
+ $this->assertRaw('ga("create", "UA-123456-3", {"name": "newTracker"});', '[testGoogleAnalyticsTrackingCode]: After codesnippet with "newTracker" tracker has been found.');
}
}
-class GoogleAnalyticsCustomVariablesTest extends DrupalWebTestCase {
+class GoogleAnalyticsCustomDimensionsAndMetricsTest extends DrupalWebTestCase {
public static function getInfo() {
return array(
- 'name' => t('Google Analytics Custom Variables tests'),
- 'description' => t('Test custom variables functionality of Google Analytics module.'),
+ 'name' => 'Google Analytics custom dimensions and metrics tests',
+ 'description' => 'Test custom dimensions and metrics functionality of Google Analytics module.',
'group' => 'Google Analytics',
'dependencies' => array('token'),
);
@@ -208,99 +286,140 @@ class GoogleAnalyticsCustomVariablesTest extends DrupalWebTestCase {
$this->admin_user = $this->drupalCreateUser($permissions);
}
- function testGoogleAnalyticsCustomVariables() {
+ function testGoogleAnalyticsCustomDimensions() {
$ua_code = 'UA-123456-3';
variable_set('googleanalytics_account', $ua_code);
// Basic test if the feature works.
- $custom_vars = array(
- 'slots' => array(
- 1 => array(
- 'slot' => 1,
- 'name' => 'Foo 1',
- 'value' => 'Bar 1',
- 'scope' => 3,
- ),
- 2 => array(
- 'slot' => 2,
- 'name' => 'Foo 2',
- 'value' => 'Bar 2',
- 'scope' => 2,
- ),
- 3 => array(
- 'slot' => 3,
- 'name' => 'Foo 3',
- 'value' => 'Bar 3',
- 'scope' => 3,
- ),
- 4 => array(
- 'slot' => 4,
- 'name' => 'Foo 4',
- 'value' => 'Bar 4',
- 'scope' => 2,
- ),
- 5 => array(
- 'slot' => 5,
- 'name' => 'Foo 5',
- 'value' => 'Bar 5',
- 'scope' => 1,
- ),
- )
+ $googleanalytics_custom_dimension = array(
+ 1 => array(
+ 'index' => 1,
+ 'value' => 'Bar 1',
+ ),
+ 2 => array(
+ 'index' => 2,
+ 'value' => 'Bar 2',
+ ),
+ 3 => array(
+ 'index' => 3,
+ 'value' => 'Bar 3',
+ ),
+ 4 => array(
+ 'index' => 4,
+ 'value' => 'Bar 4',
+ ),
+ 5 => array(
+ 'index' => 5,
+ 'value' => 'Bar 5',
+ ),
);
- variable_set('googleanalytics_custom_var', $custom_vars);
+ variable_set('googleanalytics_custom_dimension', $googleanalytics_custom_dimension);
$this->drupalGet('');
- foreach ($custom_vars['slots'] as $slot) {
- $this->assertRaw("_gaq.push(['_setCustomVar', " . $slot['slot'] . ", \"" . $slot['name'] . "\", \"" . $slot['value'] . "\", " . $slot['scope'] . "]);", '[testGoogleAnalyticsCustomVariables]: _setCustomVar ' . $slot['slot'] . ' is shown.');
+ foreach ($googleanalytics_custom_dimension as $dimension) {
+ $this->assertRaw('ga("set", ' . drupal_json_encode('dimension' . $dimension['index']) . ', ' . drupal_json_encode($dimension['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Dimension #' . $dimension['index'] . ' is shown.');
}
- // Test whether tokens are replaced in custom variable names.
+ // Test whether tokens are replaced in custom dimension values.
$site_slogan = $this->randomName(16);
variable_set('site_slogan', $site_slogan);
- $custom_vars = array(
- 'slots' => array(
- 1 => array(
- 'slot' => 1,
- 'name' => 'Name: [site:slogan]',
- 'value' => 'Value: [site:slogan]',
- 'scope' => 3,
- ),
- 2 => array(
- 'slot' => 2,
- 'name' => '',
- 'value' => $this->randomName(16),
- 'scope' => 1,
- ),
- 3 => array(
- 'slot' => 3,
- 'name' => $this->randomName(16),
- 'value' => '',
- 'scope' => 2,
- ),
- 4 => array(
- 'slot' => 4,
- 'name' => '',
- 'value' => '',
- 'scope' => 3,
- ),
- 5 => array(
- 'slot' => 5,
- 'name' => '',
- 'value' => '',
- 'scope' => 3,
- ),
- )
+ $googleanalytics_custom_dimension = array(
+ 1 => array(
+ 'index' => 1,
+ 'value' => 'Value: [site:slogan]',
+ ),
+ 2 => array(
+ 'index' => 2,
+ 'value' => $this->randomName(16),
+ ),
+ 3 => array(
+ 'index' => 3,
+ 'value' => '',
+ ),
+ // #2300701: Custom dimensions and custom metrics not outputed on zero value.
+ 4 => array(
+ 'index' => 4,
+ 'value' => '0',
+ ),
);
- variable_set('googleanalytics_custom_var', $custom_vars);
- $this->verbose('' . print_r($custom_vars, TRUE) . ''); + variable_set('googleanalytics_custom_dimension', $googleanalytics_custom_dimension); + $this->verbose('
' . print_r($googleanalytics_custom_dimension, TRUE) . ''); $this->drupalGet(''); - $this->assertRaw("_gaq.push(['_setCustomVar', 1, \"Name: $site_slogan\", \"Value: $site_slogan\", 3]", '[testGoogleAnalyticsCustomVariables]: Tokens have been replaced in custom variable.'); - $this->assertNoRaw("_gaq.push(['_setCustomVar', 2,", '[testGoogleAnalyticsCustomVariables]: Value with empty name is not shown.'); - $this->assertNoRaw("_gaq.push(['_setCustomVar', 3,", '[testGoogleAnalyticsCustomVariables]: Name with empty value is not shown.'); - $this->assertNoRaw("_gaq.push(['_setCustomVar', 4,", '[testGoogleAnalyticsCustomVariables]: Empty name and value is not shown.'); - $this->assertNoRaw("_gaq.push(['_setCustomVar', 5,", '[testGoogleAnalyticsCustomVariables]: Empty name and value is not shown.'); + $this->assertRaw('ga("set", ' . drupal_json_encode('dimension1') . ', ' . drupal_json_encode("Value: $site_slogan") . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Tokens have been replaced in dimension value.'); + $this->assertRaw('ga("set", ' . drupal_json_encode('dimension2') . ', ' . drupal_json_encode($googleanalytics_custom_dimension['2']['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Random value is shown.'); + $this->assertNoRaw('ga("set", ' . drupal_json_encode('dimension3') . ', ' . drupal_json_encode('') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Empty value is not shown.'); + $this->assertRaw('ga("set", ' . drupal_json_encode('dimension4') . ', ' . drupal_json_encode('0') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Value 0 is shown.'); + } + + function testGoogleAnalyticsCustomMetrics() { + $ua_code = 'UA-123456-3'; + variable_set('googleanalytics_account', $ua_code); + + // Basic test if the feature works. + $googleanalytics_custom_metric = array( + 1 => array( + 'index' => 1, + 'value' => '6', + 'value_expected' => 6, + ), + 2 => array( + 'index' => 2, + 'value' => '8000', + 'value_expected' => 8000, + ), + 3 => array( + 'index' => 3, + 'value' => '7.8654', + 'value_expected' => 7.8654, + ), + 4 => array( + 'index' => 4, + 'value' => '1123.4', + 'value_expected' => 1123.4, + ), + 5 => array( + 'index' => 5, + 'value' => '5,67', + 'value_expected' => 5, + ), + ); + variable_set('googleanalytics_custom_metric', $googleanalytics_custom_metric); + $this->drupalGet(''); + + foreach ($googleanalytics_custom_metric as $metric) { + $this->assertRaw('ga("set", ' . drupal_json_encode('metric' . $metric['index']) . ', ' . drupal_json_encode($metric['value_expected']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Metric #' . $metric['index'] . ' is shown.'); + } + + // Test whether tokens are replaced in custom metric values. + $googleanalytics_custom_metric = array( + 1 => array( + 'index' => 1, + 'value' => '[current-user:roles:count]', + ), + 2 => array( + 'index' => 2, + 'value' => mt_rand(), + ), + 3 => array( + 'index' => 3, + 'value' => '', + ), + // #2300701: Custom dimensions and custom metrics not outputed on zero value. + 4 => array( + 'index' => 4, + 'value' => '0', + ), + ); + variable_set('googleanalytics_custom_metric', $googleanalytics_custom_metric); + $this->verbose('
' . print_r($googleanalytics_custom_metric, TRUE) . ''); + + $this->drupalGet(''); + $this->assertRaw('ga("set", ' . drupal_json_encode('metric1') . ', ', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Tokens have been replaced in metric value.'); + $this->assertRaw('ga("set", ' . drupal_json_encode('metric2') . ', ' . drupal_json_encode($googleanalytics_custom_metric['2']['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Random value is shown.'); + $this->assertNoRaw('ga("set", ' . drupal_json_encode('metric3') . ', ' . drupal_json_encode('') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Empty value is not shown.'); + $this->assertRaw('ga("set", ' . drupal_json_encode('metric4') . ', ' . drupal_json_encode(0) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Value 0 is shown.'); } } @@ -308,8 +427,8 @@ class GoogleAnalyticsStatusMessagesTest extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => t('Google Analytics status messages tests'), - 'description' => t('Test status messages functionality of Google Analytics module.'), + 'name' => 'Google Analytics status messages tests', + 'description' => 'Test status messages functionality of Google Analytics module.', 'group' => 'Google Analytics', ); } @@ -333,9 +452,9 @@ class GoogleAnalyticsStatusMessagesTest extends DrupalWebTestCase { // Enable logging of errors only. variable_set('googleanalytics_trackmessages', array('error' => 'error')); - $this->drupalPost('user/login', array(), 'Log in'); - $this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Username field is required."]);', '[testGoogleAnalyticsStatusMessages]: _trackEvent "Username field is required." is shown.'); - $this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Password field is required."]);', '[testGoogleAnalyticsStatusMessages]: _trackEvent "Password field is required." is shown.'); + $this->drupalPost('user/login', array(), t('Log in')); + $this->assertRaw('ga("send", "event", "Messages", "Error message", "Username field is required.");', '[testGoogleAnalyticsStatusMessages]: Event message "Username field is required." is shown.'); + $this->assertRaw('ga("send", "event", "Messages", "Error message", "Password field is required.");', '[testGoogleAnalyticsStatusMessages]: Event message "Password field is required." is shown.'); // @todo: investigate why drupal_set_message() fails. //drupal_set_message('Example status message.', 'status'); @@ -343,10 +462,10 @@ class GoogleAnalyticsStatusMessagesTest extends DrupalWebTestCase { //drupal_set_message('Example error message.', 'error'); //drupal_set_message('Example error message with html tags and link.', 'error'); //$this->drupalGet(''); - //$this->assertNoRaw('_gaq.push(["_trackEvent", "Messages", "Status message", "Example status message."]);', '[testGoogleAnalyticsStatusMessages]: Example status message is not enabled for tracking.'); - //$this->assertNoRaw('_gaq.push(["_trackEvent", "Messages", "Warning message", "Example warning message."]);', '[testGoogleAnalyticsStatusMessages]: Example warning message is not enabled for tracking.'); - //$this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Example error message."]);', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.'); - //$this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Example error message with html tags and link."]);', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.'); + //$this->assertNoRaw('ga("send", "event", "Messages", "Status message", "Example status message.");', '[testGoogleAnalyticsStatusMessages]: Example status message is not enabled for tracking.'); + //$this->assertNoRaw('ga("send", "event", "Messages", "Warning message", "Example warning message.");', '[testGoogleAnalyticsStatusMessages]: Example warning message is not enabled for tracking.'); + //$this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message.");', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.'); + //$this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message with html tags and link.");', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.'); } } @@ -354,8 +473,8 @@ class GoogleAnalyticsRolesTest extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => t('Google Analytics role tests'), - 'description' => t('Test roles functionality of Google Analytics module.'), + 'name' => 'Google Analytics role tests', + 'description' => 'Test roles functionality of Google Analytics module.', 'group' => 'Google Analytics', ); } @@ -438,5 +557,165 @@ class GoogleAnalyticsRolesTest extends DrupalWebTestCase { $this->drupalGet(''); $this->assertRaw($ua_code, '[testGoogleAnalyticsRoleVisibility]: Tracking code is displayed on frontpage for included anonymous users.'); } +} + +class GoogleAnalyticsSearchTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Google Analytics search tests', + 'description' => 'Test search functionality of Google Analytics module.', + 'group' => 'Google Analytics', + ); + } + + function setUp() { + parent::setUp('googleanalytics', 'search', 'node'); + + $permissions = array( + 'access administration pages', + 'administer google analytics', + 'search content', + 'create page content', + 'edit own page content', + ); + + // User to set up google_analytics. + $this->admin_user = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->admin_user); + } + + function testGoogleAnalyticsSearchTracking() { + $ua_code = 'UA-123456-1'; + variable_set('googleanalytics_account', $ua_code); + + // Check tracking code visibility. + $this->drupalGet(''); + $this->assertRaw($ua_code, '[testGoogleAnalyticsSearch]: Tracking code is displayed for authenticated users.'); + + $this->drupalGet('search/node'); + $this->assertNoRaw('ga("set", "page",', '[testGoogleAnalyticsSearch]: Custom url not set.'); + + // Enable site search support. + variable_set('googleanalytics_site_search', 1); + + // Search for random string. + $search = array(); + $search['keys'] = $this->randomName(8); + + // Create a node to search for. + $langcode = LANGUAGE_NONE; + $edit = array(); + $edit['title'] = 'This is a test title'; + $edit["body[$langcode][0][value]"] = 'This test content contains ' . $search['keys'] . ' string.'; + + // Fire a search, it's expected to get 0 results. + $this->drupalPost('search/node', $search, t('Search')); + $this->assertRaw('ga("set", "page", (window.googleanalytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.'); + $this->assertRaw('window.googleanalytics_search_results = 0;', '[testGoogleAnalyticsSearch]: Search yielded no results.'); + + // Save the node. + $this->drupalPost('node/add/page', $edit, t('Save')); + $this->assertText(t('@type @title has been created.', array('@type' => 'Basic page', '@title' => $edit['title'])), 'Node was created.'); + + // Index the node or it cannot found. + $this->cronRun(); + + $this->drupalPost('search/node', $search, t('Search')); + $this->assertRaw('ga("set", "page", (window.googleanalytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.'); + $this->assertRaw('window.googleanalytics_search_results = 1;', '[testGoogleAnalyticsSearch]: One search result found.'); + + $this->drupalPost('node/add/page', $edit, t('Save')); + $this->assertText(t('@type @title has been created.', array('@type' => 'Basic page', '@title' => $edit['title'])), 'Node was created.'); + + // Index the node or it cannot found. + $this->cronRun(); + + $this->drupalPost('search/node', $search, t('Search')); + $this->assertRaw('ga("set", "page", (window.googleanalytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.'); + $this->assertRaw('window.googleanalytics_search_results = 2;', '[testGoogleAnalyticsSearch]: Two search results found.'); + } + +} + +class GoogleAnalyticsPhpFilterTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Google Analytics php filter tests', + 'description' => 'Test php filter functionality of Google Analytics module.', + 'group' => 'Google Analytics', + ); + } + + function setUp() { + parent::setUp('googleanalytics', 'php'); + + // Administrator with all permissions. + $permissions_admin_user = array( + 'access administration pages', + 'administer google analytics', + 'use PHP for tracking visibility', + ); + $this->admin_user = $this->drupalCreateUser($permissions_admin_user); + + // Administrator who cannot configure tracking visibility with PHP. + $permissions_delegated_admin_user = array( + 'access administration pages', + 'administer google analytics', + ); + $this->delegated_admin_user = $this->drupalCreateUser($permissions_delegated_admin_user); + } + + function testGoogleAnalyticsPhpFilter() { + $ua_code = 'UA-123456-1'; + $this->drupalLogin($this->admin_user); + + $edit = array(); + $edit['googleanalytics_account'] = $ua_code; + $edit['googleanalytics_visibility_pages'] = 2; + $edit['googleanalytics_pages'] = ''; + $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration')); + + // Compare saved setting with posted setting. + $googleanalytics_pages = variable_get('googleanalytics_pages', $this->randomName(8)); + $this->assertEqual('', $googleanalytics_pages, '[testGoogleAnalyticsPhpFilter]: PHP code snippet is intact.'); + + // Check tracking code visibility. + variable_set('googleanalytics_pages', ''); + $this->drupalGet(''); + $this->assertRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on frontpage page.'); + $this->drupalGet('admin'); + $this->assertRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on admin page.'); + + variable_set('googleanalytics_pages', ''); + $this->drupalGet(''); + $this->assertNoRaw('//www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is not displayed on frontpage page.'); + + // Test administration form. + variable_set('googleanalytics_pages', ''); + $this->drupalGet('admin/config/system/googleanalytics'); + $this->assertRaw(t('Pages on which this PHP code returns
TRUE
(experts only)'), '[testGoogleAnalyticsPhpFilter]: Permission to administer PHP for tracking visibility.');
+ $this->assertRaw(check_plain(''), '[testGoogleAnalyticsPhpFilter]: PHP code snippted is displayed.');
+
+ // Login the delegated user and check if fields are visible.
+ $this->drupalLogin($this->delegated_admin_user);
+ $this->drupalGet('admin/config/system/googleanalytics');
+ $this->assertNoRaw(t('Pages on which this PHP code returns TRUE
(experts only)'), '[testGoogleAnalyticsPhpFilter]: No permission to administer PHP for tracking visibility.');
+ $this->assertNoRaw(check_plain(''), '[testGoogleAnalyticsPhpFilter]: No permission to view PHP code snippted.');
+
+ // Set a different value and verify that this is still the same after the post.
+ variable_set('googleanalytics_pages', '');
+
+ $edit = array();
+ $edit['googleanalytics_account'] = $ua_code;
+ $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration'));
+
+ // Compare saved setting with posted setting.
+ $googleanalytics_visibility_pages = variable_get('googleanalytics_visibility_pages', 0);
+ $googleanalytics_pages = variable_get('googleanalytics_pages', $this->randomName(8));
+ $this->assertEqual(2, $googleanalytics_visibility_pages, '[testGoogleAnalyticsPhpFilter]: Pages on which this PHP code returns TRUE is selected.');
+ $this->assertEqual('', $googleanalytics_pages, '[testGoogleAnalyticsPhpFilter]: PHP code snippet is intact.');
+ }
}
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test.js b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test.js
new file mode 100644
index 00000000..05c720fb
--- /dev/null
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.test.js
@@ -0,0 +1,119 @@
+(function ($) {
+
+/**
+ * This file is for developers only.
+ *
+ * This tests are made for the javascript functions used in GA module.
+ * These tests verify if the return values are properly working.
+ *
+ * Hopefully this can be added somewhere else once Drupal core has JavaScript
+ * unit testing integrated.
+ */
+
+"use strict";
+
+Drupal.googleanalytics.test = {};
+
+Drupal.googleanalytics.test.assertSame = function (value1, value2, message) {
+ if (value1 === value2) {
+ console.info(message);
+ }
+ else {
+ console.error(message);
+ }
+};
+
+Drupal.googleanalytics.test.assertNotSame = function (value1, value2, message) {
+ if (value1 !== value2) {
+ console.info(message);
+ }
+ else {
+ console.error(message);
+ }
+};
+
+Drupal.googleanalytics.test.assertTrue = function (value1, message) {
+ if (value1 === true) {
+ console.info(message);
+ }
+ else {
+ console.error(message);
+ }
+};
+
+Drupal.googleanalytics.test.assertFalse = function (value1, message) {
+ if (value1 === false) {
+ console.info(message);
+ }
+ else {
+ console.error(message);
+ }
+};
+
+// Run after the documented is ready or Drupal.settings is undefined.
+$(document).ready(function() {
+
+ /**
+ * Run javascript tests against the GA module.
+ */
+
+ // JavaScript debugging
+ var base_url = window.location.protocol + '//' + window.location.host;
+ var base_path = window.location.pathname;
+ console.dir(Drupal);
+
+ console.group("Test 'isDownload':");
+ Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'node/8'), "Verify that '/node/8' url is not detected as file download.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip'), "Verify that '/files/foo1.zip' url is detected as a file download.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip#foo'), "Verify that '/files/foo1.zip#foo' url is detected as a file download.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip?foo=bar'), "Verify that '/files/foo1.zip?foo=bar' url is detected as a file download.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip?foo=bar#foo'), "Verify that '/files/foo1.zip?foo=bar#foo' url is detected as a file download.");
+ Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo2.ddd'), "Verify that '/files/foo2.ddd' url is not detected as file download.");
+ console.groupEnd();
+
+ console.group("Test 'isInternal':");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1'), "Link '" + base_url + Drupal.settings.basePath + "node/2' has been detected as internal link.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1#foo'), "Link '" + base_url + Drupal.settings.basePath + "node/1#foo' has been detected as internal link.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1?foo=bar'), "Link '" + base_url + Drupal.settings.basePath + "node/1?foo=bar' has been detected as internal link.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1?foo=bar#foo'), "Link '" + base_url + Drupal.settings.basePath + "node/1?foo=bar#foo' has been detected as internal link.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'go/foo'), "Link '" + base_url + Drupal.settings.basePath + "go/foo' has been detected as internal link.");
+ Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isInternal('http://example.com/node/3'), "Link 'http://example.com/node/3' has been detected as external link.");
+ console.groupEnd();
+
+ console.group("Test 'isInternalSpecial':");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternalSpecial(base_url + Drupal.settings.basePath + 'go/foo'), "Link '" + base_url + Drupal.settings.basePath + "go/foo' has been detected as special internal link.");
+ Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isInternalSpecial(base_url + Drupal.settings.basePath + 'node/1'), "Link '" + base_url + Drupal.settings.basePath + "node/1' has been detected as special internal link.");
+ console.groupEnd();
+
+ console.group("Test 'getPageUrl':");
+ Drupal.googleanalytics.test.assertSame(base_path, Drupal.googleanalytics.getPageUrl(base_url + Drupal.settings.basePath + 'node/1'), "Absolute internal URL '" + Drupal.settings.basePath + "node/1' has been extracted from full qualified url '" + base_url + base_path + "'.");
+ Drupal.googleanalytics.test.assertSame(base_path, Drupal.googleanalytics.getPageUrl(Drupal.settings.basePath + 'node/1'), "Absolute internal URL '" + Drupal.settings.basePath + "node/1' has been extracted from absolute url '" + base_path + "'.");
+ Drupal.googleanalytics.test.assertSame('http://example.com/node/2', Drupal.googleanalytics.getPageUrl('http://example.com/node/2'), "Full qualified external url 'http://example.com/node/2' has been extracted.");
+ Drupal.googleanalytics.test.assertSame('//example.com/node/2', Drupal.googleanalytics.getPageUrl('//example.com/node/2'), "Full qualified external url '//example.com/node/2' has been extracted.");
+ console.groupEnd();
+
+ console.group("Test 'getDownloadExtension':");
+ Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip'.");
+ Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip#foo'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip#foo'.");
+ Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip?foo=bar'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip?foo=bar'.");
+ Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip?foo=bar#foo'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip?foo=bar'.");
+ Drupal.googleanalytics.test.assertSame('', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo2.dddd'), "No download extension found in '" + base_url + Drupal.settings.basePath + "files/foo2.dddd'.");
+ console.groupEnd();
+
+ // List of top-level domains: example.com, example.net
+ console.group("Test 'isCrossDomain' (requires cross domain configuration with 'example.com' and 'example.net'):");
+ if (Drupal.settings.googleanalytics.trackCrossDomains) {
+ console.dir(Drupal.settings.googleanalytics.trackCrossDomains);
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isCrossDomain('example.com', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'example.com' has been found in cross domain list.");
+ Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isCrossDomain('example.net', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'example.com' has been found in cross domain list.");
+ Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isCrossDomain('www.example.com', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'www.example.com' not found in cross domain list.");
+ Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isCrossDomain('www.example.net', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'www.example.com' not found in cross domain list.");
+ }
+ else {
+ console.warn('Cross domain tracking is not enabled. Tests skipped.');
+ }
+ console.groupEnd();
+
+});
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.variable.inc b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.variable.inc
index 36885de4..6a657350 100644
--- a/sites/all/modules/contrib/admin/google_analytics/googleanalytics.variable.inc
+++ b/sites/all/modules/contrib/admin/google_analytics/googleanalytics.variable.inc
@@ -17,6 +17,7 @@ function googleanalytics_variable_info($options) {
'required' => TRUE,
'group' => 'googleanalytics',
'localize' => TRUE,
+ 'multidomain' => TRUE,
'validate callback' => 'googleanalytics_validate_googleanalytics_account',
);
@@ -44,7 +45,7 @@ function googleanalytics_validate_googleanalytics_account($variable) {
// Replace all type of dashes (n-dash, m-dash, minus) with the normal dashes.
$variable['value'] = str_replace(array('–', '—', '−'), '-', $variable['value']);
- if (!preg_match('/^UA-\d{4,}-\d+$/', $variable['value'])) {
+ if (!preg_match('/^UA-\d+-\d+$/', $variable['value'])) {
return t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.');
}
}
diff --git a/sites/all/modules/contrib/admin/module_filter/CHANGELOG.txt b/sites/all/modules/contrib/admin/module_filter/CHANGELOG.txt
index 218bc30a..c5c66703 100644
--- a/sites/all/modules/contrib/admin/module_filter/CHANGELOG.txt
+++ b/sites/all/modules/contrib/admin/module_filter/CHANGELOG.txt
@@ -1,50 +1,182 @@
-Module Filter 7.x-1.8, 2013-08-08
+Module Filter 7.x-2.x, 2015-02-20
---------------------------------
+Simplifying the table rows by hiding version and requirements until a
+ particular description is clicked.
+
+
+Module Filter 7.x-2.x, 2014-09-01
+---------------------------------
+#2235553 by greenSkin: Fixed latest jquery_update breaks module filter.
+#2304687 by mpdonadio: Fixed Remove hardcoded operations.
+#2293029 by topsitemakers: Fixed Take header offset into account when selecting
+ a tab.
+#2290213 by topsitemakers: Minor typo in description - "has no affect" -> "has
+ no effect".
+
+
+Module Filter 7.x-2.0-alpha2, 2013-12-06
+----------------------------------------
+#2141743, #2141743 by greenSkin: Fixed issues related to the new dynamically
+ positioned tabs and using the dynamically positioned save button.
+
+
+Module Filter 7.x-2.0-alpha1, 2013-11-18
+----------------------------------------
+by greenSkin: Tabs now should always be visible while scrolling large lists of
+ modules.
+#1854348 by alexweber, greenSkin: Make filter textfield wider on modules page
+ when viewing as tabs.
+by greenSkin: Fixed what was suppose to be a call to variable_set().
+#1370492 by greenSkin: Remember selected tab after modules form submit.
by greenSkin: Changed the title for the 'module_filter_dynamic_save_position'
checkbox to make it clearer what it does.
-#1933384 by kaidjohnson: Fixed jQuery UI button breaks functionality.
-#1166414 by greenSkin: Fixed module filter + tabs disabled = broken modules
- page.
-
-
-Module Filter 7.x-1.7, 2012-07-05
----------------------------------
+#1166414 by greenSkin: Fixed broken submit when tabs are disabled. We no longer
+ reroute the theme of the system modules page unless tabs are enabled.
+by greenSkin: Fixed issue on Available updates page where the update state
+ would not be remembered.
+by greenSkin: Fixed 7200 update to rebuild the theme registry at the same time
+ as rebuilding the menu.
+by greenSkin: Fixed issue relating to row coloring when enabling/disabling
+ modules via switch due to recent fix for jQuery Update module.
+by greenSkin: Added functionality to show recently enabled/disabled modules.
+#1710230 by littlekoala, greenSkin: Fixed On | Off buttons does not change
+ state with jquery_update() module active.
+by greenSkin: Added description to filter textfield on permissions page.
+by greenSkin: Added filter to user permissions page.
+by greenSkin: Fixed styling when JavaScript is disabled.
+by greenSkin: Fixed compatiblility with page_actions.
+#1351184 klonos, greenSkin: Added support for update_advanced "Ignored from
+ settings".
+#1149978 by good_man, greenSkin: Added RTL Styling for tabs and toggle switch.
+by greenSkin: Hide toggle switch when JavaScript is disabled.
+by greenSkin: Added support for ctools dropbutton as well as views styling for
+ ctools dropbutton.
+by greenSkin: Improved hash validation.
+by greenSkin: Added ability for tabs to be disabled. Direct use case is when
+ the "New" tab contains zero new modules.
+by greenSkin: Added title to "New" tab link that helps to describe the criteria
+ of a "new" module.
+#1320796 by greenSkin: Added some validation checks before trying to select a
+ tab in case the tab does not actually exist.
+#1429248 by klonos, greenSkin: Fixed Modules page table header overlaps
+ admin_menu().
+#1494694 by greenSkin: Added Let me decide if I want the cursor to focus on the
+ search box or not.
+#1515256 by catmat, greenSkin: Fixed Tabbed theme may remove functional
+ content.
+by greenSkin: Integrated dynamic positioning of the save button back in, but
+ if the module page_actions is enabled we let it handle the save button. See
+ issue #1424994.
+by greenSkin: Clicking on module name now affects the toggle switch.
+by greenSkin: Do not render the switch for incompatible modules.
#1033012 by greenSkin: Hide incompatible module rows when the 'Unavailable'
checkbox is unchecked.
-#1170388 by greenSkin: Fixed conflict with Overlay module. Added class
- "overlay-exclude" to tab links.
+by greenSkin: Performance tweak to counting the number of enabled modules.
+by greenSkin: Base new modules on the filectime of their .info file.
+#1424034 by greenSkin: Now adds the jquery.cookie.js file when needed.
+by greenSkin: Removed tweaks to the save configuration button in favor
+ http://drupal.org/project/page_actions.
+by greenSkin: Updated hook_uninstall to del all Module Filter variables.
+by greenSkin: No longer including machine name in the tab summary of modules to
+ enable/disable.
+by greenSkin: Made switches honor disabled checkboxes. A disabled switch can
+ not be turned on or off.
+by greenSkin: Added setting to toggle between using the switch or checkbox for
+ enabling/disabling modules.
+by greenSkin: Centered enable switch.
+by greenSkin: Moved couple styles to be applied via JavaScript instead of CSS
+ so they get applied once the initial loading has finished.
+by greenSkin: Implemented a "switch" look instead of checkboxes.
+by greenSkin: Remember last selected state on "Available updates" page.
+by greenSkin: Switched to using filectime() rather than filemtime() for
+ determining new modules.
+#1354134 by klonos, greenSkin: Module list now formats correctly in core
+ "Garland" theme.
+#1354134 by klonos, greenSkin: Module list now formats correctly in core
+ "Garland" theme.
#1124218 by jyee, greenSkin: Suppress form submission when hitting the enter
key while the filter input is focused.
-
-
-Module Filter 7.x-1.6, 2011-09-15
----------------------------------
-#1241662 by Niklas Fiekas: Sort modules by display name.
+by greenSkin: Simplified filter rules for updates page. Instead of checkboxes,
+ now using radios.
+by greenSkin: Fixed "All" tab to be selected by default when the page is first
+ loaded.
+#1350124 by greenSkin: Fixed filtering on package tab.
+by greenSkin: Added the "All" tab back. Added a "New" tab that lists modules
+ installed within the last week.
+by greenSkin: Fixed "No Results" not showing within selected tabs.
+#1259876 by greenSkin: Filter criteria remembered after save.
+by greenSkin: Remove deprecated function moduleGetID() from JavaScript code.
+by greenSkin: Filter now uses OR instead of AND when filtering multiply
+ queries.
+#1288590 by greenSkin: Fixed Drupal.settings.moduleFilter.enabledCounts[id] is
+ undefined.
+#1344214 by eMPee584: Fixed Notice: Undefined index: #default_value in
+ theme_module_filter_system_modules_tabs().
+#1170388 by greenSkin: Fixed confict with Overlay module. Added class
+ "overlay-exclude" to tab links. Added setting to toggle the use of a URL
+ fragment when selecting tabs.
+by greenSkin: Added README.txt.
+by greenSkin: Fixed table row striping.
+by greenSkin: Fixed regular expression to not require an operator be at the
+ beginning. Make filter queries filter with AND instead of OR. Each query will
+ further filter the list rather than potentially add to it.
+by greenSkin: Spruced up the regular expression for splitting query strings.
+by greenSkin: Added "description:" operator.
+by greenSkin: Added operator support to filter. Added "requires:" and
+ "requiredBy:" operators.
+by greenSkin: Filter now processes multiple queries separated by spaces. Use
+ quotes for a single query that includes space(s).
+by greenSkin: Fixed not updating the index when a module is enabled/disabled.
+by greenSkin: Fixed visual aid for enabling/disabling modules. Previously had
+ failed to remove +/- from tab summary.
+by Kiphaas7, greenSkin: Tabs can now be configured to hide when they contain
+ no results.
+by Kiphaas7, greenSkin: Added result info to tabs. When a filter is performed,
+ a count per tab is displayed of the number of visible results for that tab.
+by greenSkin: Now more descriptive of what modules are being enabled/disabled.
+by greenSkin: Moved module operation links to below "Requires" and
+ "Required by" section.
+by greenSkin: Added a suggest class to tabs when their module is hovered.
+by greenSkin: Distinguished difference between tab ID and hash.
+by greenSkin: Only alter the menu item 'admin/reports/updates' if it first
+ exists.
+by greenSkin: Added update to force a menu rebuild. This is needed to let
+ Module Filter alter the Update Status menu item in order to provide our
+ filter on its page.
#1254140 by greenSkin: No longer return anything in hook_update_7100.
-by greenSkin: Fixed bug with visual aids sometimes not updating correctly.
-
-
-Module Filter 7.x-1.5, 2011-08-16
----------------------------------
-by greenSkin: Brought the 7.x branch current with the 6.x branch features.
-
-
-Module Filter 7.x-1.3, 2011-03-07
----------------------------------
-by realityloop: Updated CHANGELOG.txt
-
-
-Module Filter 7.x-1.2, 2011-03-07
----------------------------------
-by realityloop: Changed placement of Submit buttons for other languages
-
-
-Module Filter 7.x-1.1, 2011-03-07
----------------------------------
-by realityloop: first commit via git, broke the release somehow :/
-
-
-Module Filter 7.x-1.0, 2011-01-04
----------------------------------
-by realityloop: Fixed Undefined index error.
-by greenSkin: Removed unused .css and .js files.
+#1257860 by greenSkin: Added filter to update status report.
+by greenSkin: Moved hiding of the inputs wrapper to css rather than a style
+ attribute.
+by greenSkin: Added missing semi-colons in JavaScript.
+by greenSkin: Changed hook comment from using "Implementation of" to
+ "Implements".
+by greenSkin: Fixed showing all modules by default when no hash is present.
+by greenSkin: Turned off autocomplete for filter textfield.
+by greenSkin: Now using "all" for hash when no tab is selected.
+by greenSkin: Implemented visual aids (displays number of modules being
+ enabled/disabled as well as coloring the modules row accordingly).
+by greenSkin: Implemented the enabled count (displays the number of enabled
+ modules of total for a package.
+by greenSkin: Updated tabs setting description.
+by greenSkin: Added missing period.
+by greenSkin: Made dynamic save position default to on. Updated element title
+ on admin page and removed "DEVELOPMENTAL" from description.
+by greenSkin: Improved fixed-top positioning when toolbar is enabled.
+by greenSkin: Added fixed classes for submit button wrapper.
+by greenSkin: Changed module-filter-tabs from a class to an id.
+by greenSkin: Fixed filter on modules page when tabs are disabled.
+by greenSkin: Set min-height to #module-filter-modules.
+by greenSkin: Implemented module_filter element and using attached js and css
+ more.
+by greenSkin: Filter input and checkboxes can now have their default values set
+ based on query params.
+by greenSkin: Tabs now use URL fragments.
+by greenSkin: Fixed regular expression used to determine tab ID.
+by greenSkin: Tabs have been re-written and are functioning.
+by greenSkin: Added Module Filter to "Administration" package.
+by greenSkin: Improved filtering performance.
+by greenSkin: New filter code.
+by greenSkin: Initial tab layout modified. Modules are all in one table but
+ look like they are in packages. All JavaScript has to do on load now is
+ remove the package name and header rows from tbody then sort the rows.
+by greenSkin: Modified the menu item's description.
diff --git a/sites/all/modules/contrib/admin/module_filter/README.txt b/sites/all/modules/contrib/admin/module_filter/README.txt
new file mode 100644
index 00000000..0bc967ab
--- /dev/null
+++ b/sites/all/modules/contrib/admin/module_filter/README.txt
@@ -0,0 +1,107 @@
+Description
+-----------
+This module provides a method for filtering modules on the modules page as well
+as for filtering projects on the update status report.
+
+The supplied filter is simpler than using your browsers find feature which
+searches the entire page. The provided filter will filter modules/projects that
+do not meet your input.
+
+Along with the filter textfield there are additional
+checkboxes that help to narrow the search more. The modules page contains four
+checkboxes: Enabled, Disabled, Required, and Unavailable. While the first two
+are self-explanatory, the latter two can take an explanation. The Required
+checkbox affects visibility of modules that are enabled and have other
+module(s) that require it also enabled. The Unavailable checkbox affects
+visibility of modules that are disabled and depend on module(s) that are
+missing.
+
+The update status report filter also contains four checkboxes: Up-to-Date,
+Update availabe, Security update, and Unknown. These directly affect the
+visibilty of each project; whether it is up-to-date, there is an update
+available, a security update is available, or the status is unknown.
+
+Installation
+------------
+To install this module, do the following:
+
+1. Extract the tar ball that you downloaded from Drupal.org.
+
+2. Upload the entire directory and all its contents to your modules directory.
+
+Configuration
+-------------
+To enable and configure this module do the following:
+
+1. Go to Admin -> Modules, and enable Module Filter.
+
+2. Go to Admin -> Configuration -> User interface -> Module filter, and make
+ any necessary configuration changes.
+
+Tabs
+----
+By default Module Filter alters the modules page into tabs (Can be disabled on
+configuration page). In the tabs view, each package is converted to a vertical
+tab rather than a fieldset which greatly increases the ability to browse them.
+
+There are several benefits to using the tabs view over the standard view for
+the modules page. I've listed the key benefits below as well as additional
+information that pertains to each.
+
+1. The increased ease of browsing between packages.
+
+2. Allows all modules to be listed alphabetically outside of their package,
+ making it all the easier to find the module by name rather than package it
+ happens to be in.
+
+3. The operations for a module are moved within the description column giving
+ the description more "elbow room".
+
+4. Filtering is restricted to within the active tab or globally when no tab is
+ selected. By default no tab is selected which will list all modules. When a
+ tab is active and you want to get back to the 'all' state click on the
+ active tab to deselect it.
+
+5. The number of enabled modules per tab is shown on the active tab. (Can be
+ disabled on configuration page)
+
+6. Nice visual aids become available showing what modules are to be
+ enabled/disabled and the number of matching modules in each tab when
+ filtering. (Can be disabled on configuration page)
+
+7. The save configuration button becomes more accessible, either staying at
+ the bottom of the window when the tabs exceed past the bottom and at the
+ top when scrolling past the tabs. (Can be disabled on configuration page)
+
+8. When filtering, tabs that do not contain matches can be hidden. (Can be
+ enabled on configuration page)
+
+9. Tab states are remembered like individual pages allowing you to move
+ forward and backward within your selections via your browsers
+ forward/backward buttons.
+
+10. When viewing all modules (no active tab) and mousing over modules it's tab
+ becomes highlighted to signify which tab it belongs to.
+
+Filter operators
+----------------
+The modules page's filter has three filter operators available. Filter
+operators allow alternative filtering techniques. A filter operator is applied
+by typing within the filter textfield 'operator:' (where operator is the
+operator type) followed immediately with the string to pass to the operator
+function (e.g. 'requires:block'). The available operators are:
+
+description:
+ Filter based on a module's description.
+
+requiredBy:
+ Filter based on what a module is required by.
+
+requires:
+ Filter based on what a module requires.
+
+Multiple filters (or queries) can be applied by space delimiting. For example,
+the filter string 'description:ctools views' would filter down to modules with
+"ctools" in the description and "views" within the module's name. To pass a
+space within a single query wrap it within double quotes (e.g. 'requires:"chaos
+tools"' or '"bulk export"').
diff --git a/sites/all/modules/contrib/admin/module_filter/css/dynamic_position.css b/sites/all/modules/contrib/admin/module_filter/css/dynamic_position.css
new file mode 100644
index 00000000..bc3b3e76
--- /dev/null
+++ b/sites/all/modules/contrib/admin/module_filter/css/dynamic_position.css
@@ -0,0 +1,23 @@
+html.js #module-filter-submit {
+ background-color: #F6F6F6;
+ width: 239px;
+ border: 1px solid #ccc;
+ border-top: 0;
+}
+html.js #module-filter-submit .form-actions {
+ text-align: center;
+ margin: 0;
+}
+html.js #module-filter-submit input {
+ margin: 2em 0 1em;
+}
+html.js #module-filter-submit.fixed {
+ position: fixed;
+ border-top: 1px solid #ccc;
+}
+html.js #module-filter-submit.fixed-top {
+ top: 0;
+}
+html.js #module-filter-submit.fixed-bottom {
+ bottom: 0;
+}
diff --git a/sites/all/modules/contrib/admin/module_filter/css/module_filter.css b/sites/all/modules/contrib/admin/module_filter/css/module_filter.css
index d2353ba0..19d113fb 100644
--- a/sites/all/modules/contrib/admin/module_filter/css/module_filter.css
+++ b/sites/all/modules/contrib/admin/module_filter/css/module_filter.css
@@ -1,4 +1,20 @@
-
+.module-filter-inputs-wrapper {
+ display: none;
+}
+.module-filter-clear {
+ display: inline;
+ position: relative;
+}
+.module-filter-clear a {
+ margin-left: 5px;
+ font-size: 11px;
+ position: absolute;
+}
#module-filter-show-wrapper .form-item {
display: inline;
}
+.module-filter-no-results {
+ text-align: center;
+ text-transform: uppercase;
+ color: #888;
+}
diff --git a/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab-rtl.css b/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab-rtl.css
new file mode 100644
index 00000000..b70f6ef0
--- /dev/null
+++ b/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab-rtl.css
@@ -0,0 +1,54 @@
+#module-filter-tabs {
+ float: right;
+}
+#module-filter-tabs li.selected a,
+#module-filter-tabs li.selected a:hover,
+#module-filter-tabs li.selected a:focus,
+#module-filter-tabs li.selected a:active {
+ background-color: #fff;
+ margin-left: -1px;
+ margin-right: 0;
+}
+.admin-operations {
+ float: left;
+}
+#module-filter-modules {
+ margin-right: 240px;
+}
+html.js .toggle-enable {
+ background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, color-stop(50%, red), color-stop(50%, orange), color-stop(100%, orange));
+ background-image: -moz-linear-gradient(right, red 50%, orange 50%, orange 100%);
+ background-image: linear-gradient(right, red 50%, orange 50%, orange 100%);
+}
+html.js .toggle-enable.enabled {
+ background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, color-stop(50%, orange), color-stop(50%, green), color-stop(100%, green));
+ background-image: -moz-linear-gradient(right, orange 50%, green 50%, green 100%);
+ background-image: linear-gradient(right, orange 50%, green 50%, green 100%);
+}
+html.js .toggle-enable.disabled {
+ background: #ccc;
+ border-color: #ddd;
+ cursor: auto;
+}
+html.js .toggle-enable.disabled div {
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%, #FEFEFE), color-stop(100%, #EEEEEE));
+ background-image: -moz-linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+ background-image: linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+}
+html.js .toggle-enable div {
+ -webkit-transition: right 0.2s;
+ -mox-transition: right 0.2s;
+ -o-transition: right 0.2s;
+ transition: right 0.2s;
+}
+html.js .toggle-enable div:before {
+ content: "ON";
+ left: -24px;
+}
+html.js .toggle-enable div:after {
+ content: "OFF";
+ left: -24px;
+}
+html.js .toggle-enable.off div {
+ right: 24px;
+}
diff --git a/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab.css b/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab.css
index 47e73370..2607a617 100644
--- a/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab.css
+++ b/sites/all/modules/contrib/admin/module_filter/css/module_filter_tab.css
@@ -1,136 +1,262 @@
+.sticky-header {
+ z-index: 1;
+}
-#module-filter-wrapper .form-item {
- border: 0px none;
+#module-filter-tabs {
+ float: left;
+}
+#module-filter-tabs ul {
+ width: 239px;
+ list-style: none;
+ list-style-image: none;
+ background-color: #ddd;
+ border: 1px solid #ccc;
+ border-top: none;
+ margin: 0;
+ padding: 0;
+ line-height: 1;
+}
+#module-filter-tabs li {
+ background: #eee;
+ border-top: 1px solid #ccc;
+ padding: 0;
+ margin: 0;
+ min-width: 0;
+}
+#module-filter-tabs li#new-tab {
+ margin-bottom: 10px;
+ border-bottom: 1px solid #ccc;
+}
+#module-filter-tabs li.disabled,
+#module-filter-tabs li#new-tab.disabled {
+ pointer-events: none;
+ cursor: default;
+ background: #ccc;
+ border-top-color: #bbb;
+ border-bottom-color: #bbb;
+}
+#module-filter-tabs li.disabled a,
+#module-filter-tabs li.disabled span {
+ color: #999;
+}
+#module-filter-tabs li.suggest {
+ background: #F9F9F9;
+}
+#module-filter-tabs li a {
+ display: block;
+ text-decoration: none;
+ padding: 10px;
+}
+#module-filter-tabs li a span.result-info {
+ float: right;
+ font-size: 10px;
+ color: #999;
+ margin-top: 3px;
+}
+#module-filter-tabs li a span.visual-aid {
+ font-size: 8px;
+/* float: right;*/
+}
+#module-filter-tabs li span.visual-aid {
+ font-weight: bold;
+}
+#module-filter-tabs li a span.enabling {
+ color: green;
+}
+#module-filter-tabs li a span.disabling {
+ color: red;
+}
+#module-filter-tabs li strong {
+ font-size: 0.923em;
+}
+#module-filter-tabs li a:hover,
+#module-filter-tabs li a:focus {
+ outline: 1px dotted;
+ background: #d5d5d5;
+ text-decoration: none;
+ outline: 0;
+}
+#module-filter-tabs li.selected a,
+#module-filter-tabs li.selected a:hover,
+#module-filter-tabs li.selected a:focus,
+#module-filter-tabs li.selected a:active {
+ background-color: #fff;
+ margin-right: -1px;
+}
+#module-filter-tabs li .summary {
+ display: block;
+ margin-bottom: 0;
+ color: #666;
+ font-size: 0.846em;
+ padding-top: 0.4em;
+}
+#module-filter-tabs li .summary .count {
+ display: none;
+}
+#module-filter-tabs li.selected .summary .count {
+ display: block;
+}
+html.js #module-filter-submit input {
+ margin: 2em 2em 1em;
+}
+html.js .module-filter-inputs-wrapper {
+ text-align: center;
+}
+html.js .module-filter-inputs-wrapper .form-item {
margin: 0;
padding: 9px;
}
-#module-filter-wrapper .form-item:after {
-/* display: block;*/
- clear: none;
+html.js .module-filter-inputs-wrapper label {
+ display: inline;
}
-#module-filter-left {
- float: left;
- background-color: #F6F6F6;
- border: 1px solid #D6DBDE;
- margin-right: -1px;
- width: 185px;
+html.js .module-filter-inputs-wrapper input[name="module_filter[name]"] {
+ width: 80%;
}
-#module-filter-left ul {
- margin: 0px;
- padding: 0px;
- list-style: none;
+html.js #module-filter-show-wrapper {
+ margin-bottom: 1em;
}
-#module-filter-left ul li {
- background: #EFEFEF none repeat scroll 0 0;;
- border-bottom: 1px solid #D6DBDE;
- margin: 0px;
- padding: 0px;
- list-style-image: none;
+html.js #module-filter-modules {
+ margin-left: 240px;
+ border: 1px solid #ccc;
}
-#module-filter-left ul li.active {
- margin-right: -1px;
- width: 186px;
- background-color: #FFFFFF;
- position: relative;
+#module-filter-modules table {
+ border-top: none;
+ border-right: none;
+ border-left: none;
}
-#module-filter-left ul li a {
- color: #777777;
- display: block;
- padding: 0.5em;
- line-height: 100%;
- font-size: 90%;
- outline: none;
-}
-#module-filter-left ul li.active a {
- background-color: #FFFFFF;
- color: #000000;
- font-weight: bold;
-}
-#module-filter-left ul li a:hover {
- background-color: #F6F6F6;
- text-decoration: none;
-}
-#module-filter-left ul li.active a:hover {
- background-color: #FFFFFF;
-}
-#module-filter-left ul li a span.visual-aid {
- font-size: 8px;
- float: right;
-}
-#module-filter-left ul li a span.enabling {
- color: green;
-}
-#module-filter-left ul li a span.disabling {
- color: red;
- margin-left: 5px;
-}
-#module-filter-left ul li a span.counts {
- font-weight: normal;
- display: none;
- font-size: 0.8em;
- color: #333333;
- padding: 2px 0 0;
-}
-#module-filter-left ul li.active a span.counts {
- display: block;
-}
-#module-filter-submit {
+html.js #module-filter-modules table {
margin: 0;
+ border-bottom: none;
}
-#module-filter-submit .form-actions {
- text-align: center;
+html.js #module-filter-modules table tr,
+html.js #module-filter-modules table td {
+ border-left: 0;
+ border-right: 0;
}
-#module-filter-submit input.form-submit {
- margin: 1em 0 0;
+#module-filter-modules table thead {
+ display: none;
}
-#module-filter-submit.fixed {
- position: fixed;
- background-color: #F6F6F6;
- border: 1px solid #D6DBDE;
- margin-left: -1px;
- width: 185px;
+#module-filter-modules table tr.admin-package-title,
+#module-filter-modules table tr.admin-package-title td {
+ border: none !important;
+ border-top: 1px solid #ccc !important;
+ background-color: transparent !important;
+ padding: 10px 0 0;
}
-#module-filter-submit.fixed-top {
- top: 0;
+#module-filter-modules table tr.admin-package-title.first,
+#module-filter-modules table tr.admin-package-title.first td {
+ border-top: none !important;
}
-#module-filter-submit.fixed-bottom {
- bottom: 0;
+#module-filter-modules table tr.admin-package-header td {
+ border: 1px solid #ccc;
+ text-transform: uppercase;
+ background: #E1E2DC;
+ font-weight: normal;
+ padding: 3px 10px;
}
-#module-filter-right {
- display: block;
-}
-#module-filter-squeeze {
- margin-left: 186px;
- background-color: #FFFFFF;
- border: 1px solid #D6DBDE;
- height: auto !important;
-}
-.form-item-module-filter-name {
- text-align: center;
-}
-.form-item-module-filter-name label {
- display: inline;
-}
-#module-filter-show-wrapper .form-checkboxes {
- text-align: center;
-}
-#module-filter-show-wrapper .form-item:after {
- display: inline;
-}
-table.package {
- margin: 1em 0;
-}
-table.package,
-table.package thead,
-table.package tbody,
-table.package tbody tr,
-table.package td:last-child {
- border-right: 0 none;
- border-left: 0 none;
-}
-table.package tr.enabling {
+#module-filter-modules table tr.enabling {
background-color: #dfd;
}
-table.package tr.disabling {
+#module-filter-modules table tr.disabling {
background-color: #fcc;
}
+#module-filter-modules span.module-machine-name {
+ font-size: 0.9em;
+ font-weight: normal;
+}
+.admin-version {
+ white-space: nowrap;
+}
+.admin-operations {
+ float: right;
+}
+.admin-operations a.module-link {
+ display: inline;
+}
+
+html.js .toggle-enable {
+ margin: auto;
+ position: relative;
+ width: 50px;
+ overflow: hidden;
+ height: 18px;
+ line-height: 18px;
+ font-size: 11px;
+ text-align: center;
+ cursor: pointer;
+ border: 1px solid #ccc;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ -khtml-border-radius: 3px;
+ border-radius: 3px;
+ -moz-box-shadow: 0 0 10px rgba(0,0,0,0.50) inset;
+ -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.50) inset;
+ box-shadow: 0 0 10px rgba(0,0,0,0.50) inset;
+ background-clip: padding-box;
+ background-image: -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(50%, red), color-stop(50%, orange), color-stop(100%, orange));
+ background-image: -moz-linear-gradient(left, red 50%, orange 50%, orange 100%);
+ background-image: linear-gradient(left, red 50%, orange 50%, orange 100%);
+}
+html.js .toggle-enable.enabled {
+ background-image: -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(50%, orange), color-stop(50%, green), color-stop(100%, green));
+ background-image: -moz-linear-gradient(left, orange 50%, green 50%, green 100%);
+ background-image: linear-gradient(left, orange 50%, green 50%, green 100%);
+}
+html.js .toggle-enable.disabled {
+ background: #ccc;
+ border-color: #ddd;
+ cursor: auto;
+}
+html.js .toggle-enable div {
+ position: relative;
+ color: #777;
+ width: 26px;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ -khtml-border-radius: 2px;
+ border-radius: 2px;
+ background: white;
+ text-shadow: 1px 1px 0 white;
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%, #FEFEFE), color-stop(100%, #EAEAEA));
+ background-image: -moz-linear-gradient(top, #FEFEFE 0%, #EAEAEA 100%);
+ background-image: linear-gradient(top, #FEFEFE 0%, #EAEAEA 100%);
+ -webkit-transition: left 0.2s;
+ -mox-transition: left 0.2s;
+ -o-transition: left 0.2s;
+ transition: left 0.2s;
+}
+html.js .toggle-enable.disabled div {
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%, #FEFEFE), color-stop(100%, #EEEEEE));
+ background-image: -moz-linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+ background-image: linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+}
+html.js .toggle-enable div:after,
+html.js .toggle-enable div:before {
+ color: white;
+ text-shadow: none;
+ width: 25px;
+ position: absolute;
+ top: 0;
+ font-size: 9px;
+ font-weight: bold;
+}
+html.js .toggle-enable div:before {
+ content: "OFF";
+ left: -24px;
+}
+html.js .toggle-enable div:after {
+ content: "ON";
+ right: -24px;
+}
+html.js .toggle-enable.off div {
+ left: 24px;
+}
+
+#module-filter-tabs.top-fixed {
+ position: fixed;
+ top: 0;
+}
+#module-filter-tabs.bottom-fixed {
+ position: fixed;
+ bottom: 0;
+}
diff --git a/sites/all/modules/contrib/admin/module_filter/css/modules.css b/sites/all/modules/contrib/admin/module_filter/css/modules.css
new file mode 100644
index 00000000..91fa7418
--- /dev/null
+++ b/sites/all/modules/contrib/admin/module_filter/css/modules.css
@@ -0,0 +1,47 @@
+#system-modules table {
+ table-layout: fixed;
+}
+#system-modules th.checkbox {
+ width: 8%;
+}
+#system-modules th.name {
+ width: 25%;
+}
+#system-modules th.links {
+ width: 15%;
+}
+#system-modules td {
+ vertical-align: top;
+}
+#system-modules .expand.inner {
+ background: transparent url(/misc/menu-collapsed.png) left 0.6em no-repeat;
+ margin-left: -12px;
+ padding-left: 12px;
+}
+#system-modules .expanded.expand.inner {
+ background: transparent url(/misc/menu-expanded.png) left 0.6em no-repeat;
+}
+#system-modules .description {
+ cursor: pointer;
+}
+#system-modules .description .inner {
+ overflow: hidden; /* truncates descriptions if too long */
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+#system-modules .description .requirements,
+#system-modules .description .links {
+ display: none;
+}
+#system-modules .description .expanded.inner {
+ overflow: visible;
+ white-space: normal;
+}
+#system-modules .description .expanded .requirements,
+#system-modules .description .expanded .links {
+ display: block;
+}
+#system-modules .requirements {
+ padding: 5px 0;
+ max-width: 490px;
+}
diff --git a/sites/all/modules/contrib/admin/module_filter/css/update_status.css b/sites/all/modules/contrib/admin/module_filter/css/update_status.css
new file mode 100644
index 00000000..c0bee443
--- /dev/null
+++ b/sites/all/modules/contrib/admin/module_filter/css/update_status.css
@@ -0,0 +1,18 @@
+#module-filter-update-status-form {
+ float: right;
+}
+.module-filter-inputs-wrapper label {
+ display: inline;
+}
+.module-filter-inputs-wrapper .form-item-module-filter-name {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ text-align: right;
+}
+#module-filter-show-wrapper .form-item {
+ padding: 5px;
+}
+p.module-filter-no-results {
+ clear: both;
+ padding-top: 30px;
+}
diff --git a/sites/all/modules/contrib/admin/module_filter/js/dynamic_position.js b/sites/all/modules/contrib/admin/module_filter/js/dynamic_position.js
index 10c66b4f..41c57312 100644
--- a/sites/all/modules/contrib/admin/module_filter/js/dynamic_position.js
+++ b/sites/all/modules/contrib/admin/module_filter/js/dynamic_position.js
@@ -1,33 +1,48 @@
(function($) {
- Drupal.behaviors.moduleFilterDynamicPosition = {
- attach: function() {
- $(window).scroll(function() {
+
+Drupal.behaviors.moduleFilterDynamicPosition = {
+ attach: function(context) {
+ var $window = $(window);
+
+ $('#module-filter-wrapper', context).once('dynamic-position', function() {
+ // Move the submit button just below the tabs.
+ $('#module-filter-tabs').append($('#module-filter-submit'));
+
+ var positionSubmit = function() {
+ var $tabs = $('#module-filter-tabs');
+ var $submit = $('#module-filter-submit', $tabs);
+
// Vertical movement.
- var top = $('#module-filter-tabs').offset().top;
- var bottom = top + $('#module-filter-tabs').height();
- var windowHeight = $(window).height();
- if (((bottom - windowHeight) > ($(window).scrollTop() - $('#module-filter-submit').height())) && $(window).scrollTop() + windowHeight - $('#module-filter-submit').height() - $('#all-tab').height() > top) {
- $('#module-filter-submit').removeClass('fixed-top').addClass('fixed fixed-bottom');
+ var bottom = $tabs.offset().top + $tabs.outerHeight();
+ if ($submit.hasClass('fixed-bottom')) {
+ bottom += $submit.height();
}
- else if (bottom < $(window).scrollTop()) {
- $('#module-filter-submit').removeClass('fixed-bottom').addClass('fixed fixed-top');
+ if (bottom >= $window.height() + $window.scrollTop()) {
+ $submit.addClass('fixed fixed-bottom');
+ $tabs.css('padding-bottom', $submit.height());
}
else {
- $('#module-filter-submit').removeClass('fixed fixed-bottom fixed-top');
+ $submit.removeClass('fixed fixed-bottom');
+ $tabs.css('padding-bottom', 0);
}
// Horizontal movement.
- if ($('#module-filter-submit').hasClass('fixed-bottom') || $('#module-filter-submit').hasClass('fixed-top')) {
- var left = $('#module-filter-tabs').offset().left - $(window).scrollLeft();
- if (left != $('#module-filter-submit').offset().left - $(window).scrollLeft()) {
- $('#module-filter-submit').css('left', left);
+ if ($submit.hasClass('fixed-bottom') || $submit.hasClass('fixed-top')) {
+ var left = $tabs.offset().left - $window.scrollLeft();
+ if (left != $submit.offset().left - $window.scrollLeft()) {
+ $submit.css('left', left);
}
}
- });
- $(window).trigger('scroll');
- $(window).resize(function() {
- $(window).trigger('scroll');
- });
- }
+ };
+
+ // Control the positioning.
+ $window.scroll(positionSubmit);
+ $window.resize(positionSubmit);
+ var moduleFilter = $('input[name="module_filter[name]"]').data('moduleFilter');
+ moduleFilter.element.bind('moduleFilter:adjustHeight', positionSubmit);
+ moduleFilter.adjustHeight();
+ });
}
+};
+
})(jQuery);
diff --git a/sites/all/modules/contrib/admin/module_filter/js/module_filter.js b/sites/all/modules/contrib/admin/module_filter/js/module_filter.js
index 49f6b7a0..d5f53287 100644
--- a/sites/all/modules/contrib/admin/module_filter/js/module_filter.js
+++ b/sites/all/modules/contrib/admin/module_filter/js/module_filter.js
@@ -1,120 +1,271 @@
-
(function ($) {
- var moduleFilterTimeOut;
- var moduleFilterTextFilter = '';
- Drupal.behaviors.moduleFilter = {
- attach: function() {
- $("#module-filter-wrapper").show();
- $('input[name="module_filter[name]"]').focus();
- $('input[name="module_filter[name]"]').keyup(function(e) {
- switch (e.which) {
- case 13:
- if (moduleFilterTimeOut) {
- clearTimeout(moduleFilterTimeOut);
- }
+Drupal.ModuleFilter = {};
- moduleFilter(moduleFilterTextFilter);
- break;
- default:
- if (moduleFilterTextFilter != $(this).val()) {
- moduleFilterTextFilter = this.value;
- if (moduleFilterTimeOut) {
- clearTimeout(moduleFilterTimeOut);
- }
+Drupal.ModuleFilter.explode = function(string) {
+ var queryArray = string.match(/([a-zA-Z]+\:(\w+|"[^"]+")*)|\w+|"[^"]+"/g);
+ if (!queryArray) {
+ queryArray = new Array();
+ }
+ var i = queryArray.length;
+ while (i--) {
+ queryArray[i] = queryArray[i].replace(/"/g, "");
+ }
+ return queryArray;
+};
- moduleFilterTimeOut = setTimeout('moduleFilter("' + moduleFilterTextFilter + '")', 500);
- }
- break;
+Drupal.ModuleFilter.getState = function(key) {
+ if (!Drupal.ModuleFilter.state) {
+ Drupal.ModuleFilter.state = {};
+ var cookie = $.cookie('DrupalModuleFilter');
+ var query = cookie ? cookie.split('&') : [];
+ if (query) {
+ for (var i in query) {
+ // Extra check to avoid js errors in Chrome, IE and Safari when
+ // combined with JS like twitter's widget.js.
+ // See http://drupal.org/node/798764.
+ if (typeof(query[i]) == 'string' && query[i].indexOf('=') != -1) {
+ var values = query[i].split('=');
+ if (values.length === 2) {
+ Drupal.ModuleFilter.state[values[0]] = values[1];
+ }
}
- });
- $('input[name="module_filter[name]"]').keypress(function(e) {
- if (e.which == 13) e.preventDefault();
- });
-
- $('#edit-module-filter-show-enabled').change(function() {
- moduleFilter($('input[name="module_filter[name]"]').val());
- });
- $('#edit-module-filter-show-disabled').change(function() {
- moduleFilter($('input[name="module_filter[name]"]').val());
- });
- $('#edit-module-filter-show-required').change(function() {
- moduleFilter($('input[name="module_filter[name]"]').val());
- });
- $('#edit-module-filter-show-unavailable').change(function() {
- moduleFilter($('input[name="module_filter[name]"]').val());
- });
+ }
}
}
+ return Drupal.ModuleFilter.state[key] ? Drupal.ModuleFilter.state[key] : false;
+};
- moduleFilter = function(string) {
- stringLowerCase = string.toLowerCase();
+Drupal.ModuleFilter.setState = function(key, value) {
+ var existing = Drupal.ModuleFilter.getState(key);
+ if (existing != value) {
+ Drupal.ModuleFilter.state[key] = value;
+ var query = [];
+ for (var i in Drupal.ModuleFilter.state) {
+ query.push(i + '=' + Drupal.ModuleFilter.state[i]);
+ }
+ $.cookie('DrupalModuleFilter', query.join('&'), { expires: 7, path: '/' });
+ }
+};
- $("fieldset table tbody tr td label strong").each(function(i) {
- var $row = $(this).parents('tr');
- var module = $(this).text();
- var moduleLowerCase = module.toLowerCase();
- var $fieldset = $row.parents('fieldset');
+Drupal.ModuleFilter.Filter = function(element, selector, options) {
+ var self = this;
- if (string != '') {
- if ($fieldset.hasClass('collapsed')) {
- $fieldset.removeClass('collapsed');
+ this.element = element;
+ this.text = $(this.element).val();
+
+ this.settings = Drupal.settings.moduleFilter;
+
+ this.selector = selector;
+
+ this.options = $.extend({
+ delay: 500,
+ striping: false,
+ childSelector: null,
+ empty: Drupal.t('No results'),
+ rules: new Array()
+ }, options);
+ if (this.options.wrapper == undefined) {
+ this.options.wrapper = $(self.selector).parent();
+ }
+
+ // Add clear button.
+ this.element.after('');
+ if (this.text) {
+ $('.module-filter-clear a', this.element.parent()).removeClass('js-hide');
+ }
+ $('.module-filter-clear a', this.element.parent()).click(function() {
+ self.element.val('');
+ self.text = '';
+ delete self.queries;
+ self.applyFilter();
+ self.element.focus();
+ $(this).addClass('js-hide');
+ return false;
+ });
+
+ this.updateQueries = function() {
+ var queryStrings = Drupal.ModuleFilter.explode(self.text);
+
+ self.queries = new Array();
+ for (var i in queryStrings) {
+ var query = { operator: 'text', string: queryStrings[i] };
+
+ if (self.operators != undefined) {
+ // Check if an operator is possibly used.
+ if (queryStrings[i].indexOf(':') > 0) {
+ // Determine operator used.
+ var args = queryStrings[i].split(':', 2);
+ var operator = args.shift();
+ if (self.operators[operator] != undefined) {
+ query.operator = operator;
+ query.string = args.shift();
+ }
}
}
- if (moduleLowerCase.match(stringLowerCase)) {
- if (moduleFilterVisible($('td.checkbox input', $row))) {
- if (!$row.is(':visible')) {
- $row.show();
- if ($fieldset.hasClass('collapsed')) {
- $fieldset.removeClass('collapsed');
- }
- if (!$fieldset.is(':visible')) {
- $fieldset.show();
- }
+ query.string = query.string.toLowerCase();
+
+ self.queries.push(query);
+ }
+
+ if (self.queries.length <= 0) {
+ // Add a blank string query.
+ self.queries.push({ operator: 'text', string: '' });
+ }
+ };
+
+ this.applyFilter = function() {
+ self.results = new Array();
+
+ self.updateQueries();
+
+ if (self.index == undefined) {
+ self.buildIndex();
+ }
+
+ self.element.trigger('moduleFilter:start');
+
+ $.each(self.index, function(key, item) {
+ var $item = item.element;
+
+ for (var i in self.queries) {
+ var query = self.queries[i];
+ if (query.operator == 'text') {
+ if (item.text.indexOf(query.string) < 0) {
+ continue;
}
}
else {
- $row.hide();
- if ($row.siblings(':visible').html() == null) {
- $fieldset.hide();
+ var func = self.operators[query.operator];
+ if (!(func(query.string, self, item))) {
+ continue;
}
}
- }
- else {
- if ($row.is(':visible')) {
- $row.hide();
- if ($row.siblings(':visible').html() == null) {
- $fieldset.hide();
- }
- }
- }
- });
- }
- moduleFilterVisible = function(checkbox) {
- if (checkbox.length > 0) {
- if ($('#edit-module-filter-show-enabled').is(':checked')) {
- if ($(checkbox).is(':checked') && !$(checkbox).is(':disabled')) {
+ var rulesResult = self.processRules(item);
+ if (rulesResult !== false) {
return true;
}
}
- if ($('#edit-module-filter-show-disabled').is(':checked')) {
- if (!$(checkbox).is(':checked') && !$(checkbox).is(':disabled')) {
- return true;
+
+ $item.addClass('js-hide');
+ });
+
+ self.element.trigger('moduleFilter:finish', { results: self.results });
+
+ if (self.options.striping) {
+ self.stripe();
+ }
+
+ if (self.results.length > 0) {
+ self.options.wrapper.find('.module-filter-no-results').remove();
+ }
+ else {
+ if (!self.options.wrapper.find('.module-filter-no-results').length) {
+ self.options.wrapper.append($('').text(self.options.empty));
+ };
+ }
+ };
+
+ self.element.keyup(function(e) {
+ switch (e.which) {
+ case 13:
+ if (self.timeOut) {
+ clearTimeout(self.timeOut);
}
- }
- if ($('#edit-module-filter-show-required').is(':checked')) {
- if ($(checkbox).is(':checked') && $(checkbox).is(':disabled')) {
- return true;
+ self.applyFilter();
+ break;
+ default:
+ if (self.text != $(this).val()) {
+ if (self.timeOut) {
+ clearTimeout(self.timeOut);
+ }
+
+ self.text = $(this).val();
+
+ if (self.text) {
+ self.element.parent().find('.module-filter-clear a').removeClass('js-hide');
+ }
+ else {
+ self.element.parent().find('.module-filter-clear a').addClass('js-hide');
+ }
+
+ self.element.trigger('moduleFilter:keyup');
+
+ self.timeOut = setTimeout(self.applyFilter, self.options.delay);
}
+ break;
+ }
+ });
+
+ self.element.keypress(function(e) {
+ if (e.which == 13) e.preventDefault();
+ });
+};
+
+Drupal.ModuleFilter.Filter.prototype.buildIndex = function() {
+ var self = this;
+ var index = new Array();
+ $(this.selector).each(function(i) {
+ var text = (self.options.childSelector) ? $(self.options.childSelector, this).text() : $(this).text();
+ var item = {
+ key: i,
+ element: $(this),
+ text: text.toLowerCase()
+ };
+ for (var j in self.options.buildIndex) {
+ var func = self.options.buildIndex[j];
+ item = $.extend(func(self, item), item);
+ }
+ $(this).data('indexKey', i);
+ index.push(item);
+ delete item;
+ });
+ this.index = index;
+};
+
+Drupal.ModuleFilter.Filter.prototype.processRules = function(item) {
+ var self = this;
+ var $item = item.element;
+ var rulesResult = true;
+ if (self.options.rules.length > 0) {
+ for (var i in self.options.rules) {
+ var func = self.options.rules[i];
+ rulesResult = func(self, item);
+ if (rulesResult === false) {
+ break;
}
}
- if ($('#edit-module-filter-show-unavailable').is(':checked')) {
- if (checkbox.length == 0 || ($(checkbox).size() > 0 && !$(checkbox).is(':checked') && $(checkbox).is(':disabled'))) {
- return true;
- }
- }
- return false;
}
+ if (rulesResult !== false) {
+ $item.removeClass('js-hide');
+ self.results.push(item);
+ }
+ return rulesResult;
+};
+
+Drupal.ModuleFilter.Filter.prototype.stripe = function() {
+ var self = this;
+ var flip = { even: 'odd', odd: 'even' };
+ var stripe = 'odd';
+
+ $.each(self.index, function(key, item) {
+ if (!item.element.hasClass('js-hide')) {
+ item.element.removeClass('odd even')
+ .addClass(stripe);
+ stripe = flip[stripe];
+ }
+ });
+};
+
+$.fn.moduleFilter = function(selector, options) {
+ var filterInput = this;
+ filterInput.parents('.module-filter-inputs-wrapper').show();
+ if (Drupal.settings.moduleFilter.setFocus) {
+ filterInput.focus();
+ }
+ filterInput.data('moduleFilter', new Drupal.ModuleFilter.Filter(this, selector, options));
+};
+
})(jQuery);
diff --git a/sites/all/modules/contrib/admin/module_filter/js/module_filter_tab.js b/sites/all/modules/contrib/admin/module_filter/js/module_filter_tab.js
index a6024453..aa54b983 100644
--- a/sites/all/modules/contrib/admin/module_filter/js/module_filter_tab.js
+++ b/sites/all/modules/contrib/admin/module_filter/js/module_filter_tab.js
@@ -1,281 +1,562 @@
(function ($) {
- Drupal.ModuleFilter = Drupal.ModuleFilter || {};
- Drupal.ModuleFilter.textFilter = '';
- Drupal.ModuleFilter.timeout;
- Drupal.ModuleFilter.tabs = {};
- Drupal.ModuleFilter.enabling = {};
- Drupal.ModuleFilter.disabling = {};
- Drupal.behaviors.moduleFilter = {
- attach: function() {
- // Set the focus on the module filter textfield.
- $('input[name="module_filter[name]"]').focus();
+Drupal.ModuleFilter.tabs = {};
+Drupal.ModuleFilter.enabling = {};
+Drupal.ModuleFilter.disabling = {};
- $('#module-filter-squeeze').css('min-height', $('#module-filter-tabs').height());
+Drupal.ModuleFilter.jQueryIsNewer = function() {
+ if (Drupal.ModuleFilter.jQueryNewer == undefined) {
+ var v1parts = $.fn.jquery.split('.');
+ var v2parts = new Array('1', '4', '4');
- $('#module-filter-left a.project-tab').each(function(i) {
- Drupal.ModuleFilter.tabs[$(this).attr('id')] = new Drupal.ModuleFilter.Tab(this);
- });
+ for (var i = 0; i < v1parts.length; ++i) {
+ if (v2parts.length == i) {
+ Drupal.ModuleFilter.jQueryNewer = true;
+ return Drupal.ModuleFilter.jQueryNewer;
+ }
- // Move anchors to top of tabs.
- $('a.anchor', $('#module-filter-left')).remove().prependTo('#module-filter-tabs');
+ if (v1parts[i] == v2parts[i]) {
+ continue;
+ }
+ else if (v1parts[i] > v2parts[i]) {
+ Drupal.ModuleFilter.jQueryNewer = true;
+ return Drupal.ModuleFilter.jQueryNewer;
+ }
+ else {
+ Drupal.ModuleFilter.jQueryNewer = false;
+ return Drupal.ModuleFilter.jQueryNewer;
+ }
+ }
- $('input[name="module_filter[name]"]').keyup(function(e) {
- switch (e.which) {
- case 13:
- if (Drupal.ModuleFilter.timeout) {
- clearTimeout(Drupal.ModuleFilter.timeout);
- }
+ if (v1parts.length != v2parts.length) {
+ Drupal.ModuleFilter.jQueryNewer = false;
+ return Drupal.ModuleFilter.jQueryNewer;
+ }
- Drupal.ModuleFilter.filter(Drupal.ModuleFilter.textFilter);
- break;
- default:
- if (Drupal.ModuleFilter.textFilter != $(this).val()) {
- Drupal.ModuleFilter.textFilter = this.value;
- if (Drupal.ModuleFilter.timeout) {
- clearTimeout(Drupal.ModuleFilter.timeout);
+ Drupal.ModuleFilter.jQueryNewer = false;
+ }
+ return Drupal.ModuleFilter.jQueryNewer;
+};
+
+Drupal.behaviors.moduleFilterTabs = {
+ attach: function(context) {
+ if (Drupal.settings.moduleFilter.tabs) {
+ $('#module-filter-wrapper table:not(.sticky-header)', context).once('module-filter-tabs', function() {
+ var $modules = $('#module-filter-modules');
+ var moduleFilter = $('input[name="module_filter[name]"]').data('moduleFilter');
+ var table = $(this);
+
+ $('thead', table).show();
+
+ // Remove package header rows.
+ $('tr.admin-package-header', table).remove();
+
+ var $tabsWrapper = $('');
+
+ // Build tabs from package title rows.
+ var tabs = '' . t('This is a list of defined user functions that generated this current request lifecycle. Click on a function name to view its documentation.') . '
'; case 'devel/session': - return ''. t('Here are the contents of your $_SESSION
variable.') .'
' . t('Here are the contents of your $_SESSION
variable.') . '
'. t('This is a list of the variables and their values currently stored in variables table and the $conf
array of your settings.php file. These variables are usually accessed with variable_get() and variable_set(). Variables that are too long can slow down your pages.', array('@variable-get-doc' => "http://$api/api/HEAD/function/variable_get", '@variable-set-doc' => "http://$api/api/HEAD/function/variable_set")) .'
' . t('This is a list of the variables and their values currently stored in variables table and the $conf
array of your settings.php file. These variables are usually accessed with variable_get() and variable_set(). Variables that are too long can slow down your pages.', array('@variable-get-doc' => "http://$api/api/HEAD/function/variable_get", '@variable-set-doc' => "http://$api/api/HEAD/function/variable_set")) . '
'. $name . $output .''; + $printed_value = '
' . $name . $output . ''; } if ($return) { @@ -1809,11 +1990,26 @@ function devel_render() { } /** - * Print the function call stack. + * Prints the function call stack. + * + * @param $return + * Pass TRUE to return the formatted backtrace rather than displaying it in + * the browser via kprint_r(). + * @param $pop + * How many items to pop from the top of the stack; useful when calling from + * an error handler. + * @param $options + * Options to pass on to PHP's debug_backtrace(), depending on your PHP + * version. + * + * @return string|NULL + * The formatted backtrace, if requested, or NULL. + * + * @see http://php.net/manual/en/function.debug-backtrace.php */ -function ddebug_backtrace($return = FALSE, $pop = 0) { +function ddebug_backtrace($return = FALSE, $pop = 0, $options = TRUE) { if (user_access('access devel information')) { - $backtrace = debug_backtrace(); + $backtrace = debug_backtrace($options); while ($pop-- > 0) { array_shift($backtrace); } @@ -1847,6 +2043,7 @@ function ddebug_backtrace($return = FALSE, $pop = 0) { else { $function = $backtrace[1]['function'] . '()'; } + $backtrace[1] += array('args' => array()); $call['args'] = $backtrace[1]['args']; } else { @@ -1864,31 +2061,33 @@ function ddebug_backtrace($return = FALSE, $pop = 0) { } } -// Delete all files in a dir. +/** + * Deletes all files in a dir. + */ function devel_empty_dir($dir) { - foreach (new DirectoryIterator($dir) as $fileInfo) { - if ($fileInfo->isFile()) { - unlink($fileInfo->getPathname()); + foreach (new DirectoryIterator($dir) as $file_info) { + if ($file_info->isFile()) { + unlink($file_info->getPathname()); } } } /* - * migration related functions + * Migration-related functions. */ /** - * Regenerate the data in node_comment_statistics table. Technique comes from - * http://www.artfulsoftware.com/infotree/queries.php?&bw=1280#101 + * Regenerates the data in node_comment_statistics table. + * Technique - http://www.artfulsoftware.com/infotree/queries.php?&bw=1280#101 * * @return void - **/ + */ function devel_rebuild_node_comment_statistics() { - // Empty table + // Empty table. db_truncate('node_comment_statistics')->execute(); - // TODO: DBTNG. Ignore keyword is Mysql only? Is only used in the rare case when - // two comments on the same node share same timestamp. + // TODO: DBTNG. Ignore keyword is Mysql only? Is only used in the rare case + // when two comments on the same node share same timestamp. $sql = " INSERT IGNORE INTO {node_comment_statistics} (nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) ( SELECT c.nid, c.cid, c.created, c.name, c.uid, c2.comment_count FROM {comment} c @@ -1908,7 +2107,7 @@ function devel_rebuild_node_comment_statistics() { $query->addExpression('NULL', 'last_comment_name'); $query->isNull('ncs.comment_count'); - db_insert('node_comment_statistics') + db_insert('node_comment_statistics', array('return' => Database::RETURN_NULL)) ->from($query) ->execute(); } @@ -1935,8 +2134,7 @@ function devel_form_user_admin_permissions_alter(&$form, &$form_state) { /** * Implements hook_form_alter(). * - * Adds mouse-over hints on the Modules page to display - * module base names. + * Adds mouse-over hints on the Modules page to display module base names. */ function devel_form_system_modules_alter(&$form, &$form_state) { if (user_access('access devel information') && variable_get('devel_raw_names', FALSE) && isset($form['modules']) && is_array($form['modules'])) { diff --git a/sites/all/modules/contrib/dev/devel/devel.pages.inc b/sites/all/modules/contrib/dev/devel/devel.pages.inc index 9870f520..7a137da3 100644 --- a/sites/all/modules/contrib/dev/devel/devel.pages.inc +++ b/sites/all/modules/contrib/dev/devel/devel.pages.inc @@ -145,20 +145,17 @@ function devel_field_info_page() { $output .= kprint_r($info, TRUE, t('Instances')); $info = field_info_bundles(); $output .= kprint_r($info, TRUE, t('Bundles')); + $info = field_info_field_types(); + $output .= kprint_r($info, TRUE, t('Field types')); + $info = field_info_formatter_types(); + $output .= kprint_r($info, TRUE, t('Formatter types')); + $info = field_info_storage_types(); + $output .= kprint_r($info, TRUE, t('Storage types')); + $info = field_info_widget_types(); + $output .= kprint_r($info, TRUE, t('Widget types')); return $output; } -/** - * Menu callback; display all variables. - */ -function devel_variable_page() { - // We return our own $page so as to avoid blocks. - $output = drupal_get_form('devel_variable_form'); - drupal_set_page_content($output); - $page = element_info('page'); - return $page; -} - function devel_variable_form() { $header = array( 'name' => array('data' => t('Name'), 'field' => 'name', 'sort' => 'asc'), diff --git a/sites/all/modules/contrib/dev/devel/devel.test b/sites/all/modules/contrib/dev/devel/devel.test index 7d66bf7f..50defcf6 100644 --- a/sites/all/modules/contrib/dev/devel/devel.test +++ b/sites/all/modules/contrib/dev/devel/devel.test @@ -45,7 +45,8 @@ class DevelMailTest extends DrupalWebTestCase { $this->assertEqual($content, 'From: postmaster@example.com X-stupid: dumb To: drupal@example.com -Test mail +Subject: Test mail + I am the body of this message'); } diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.drush.inc b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.drush.inc index f90a57ad..65ae3822 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.drush.inc +++ b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.drush.inc @@ -10,6 +10,7 @@ */ function devel_generate_drush_command() { $items['generate-users'] = array( + 'callback' => 'drush_devel_generate_users', 'description' => 'Create users.', 'arguments' => array( 'number_users' => 'Number of users to generate.', @@ -17,10 +18,12 @@ function devel_generate_drush_command() { 'options' => array( 'kill' => 'Delete all users before generating new ones.', 'roles' => 'A comma delimited list of role IDs which should be granted to the new users. No need to specify authenticated user role.', + 'pass' => 'Specify a password to be set for all generated users.', ), 'aliases' => array('genu'), ); $items['generate-terms'] = array( + 'callback' => 'drush_devel_generate_terms', 'description' => 'Create terms in specified vocabulary.', 'arguments' => array( 'machine_name' => 'Vocabulary machine name into which new terms will be inserted.', @@ -28,12 +31,13 @@ function devel_generate_drush_command() { ), 'options' => array( 'kill' => 'Delete all terms in specified vocabulary before generating.', - 'feedback' => 'An integer representing interval for insertion rate logging. Defaults to 500', + 'feedback' => 'An integer representing interval for insertion rate logging. Defaults to 1000', ), 'aliases' => array('gent'), ); $items['generate-vocabs'] = array( + 'callback' => 'drush_devel_generate_vocabs', 'description' => 'Create vocabularies.', 'arguments' => array( 'num_vocabs' => 'Number of vocabularies to create. Defaults to 1.', @@ -44,6 +48,7 @@ function devel_generate_drush_command() { 'aliases' => array('genv'), ); $items['generate-content'] = array( + 'callback' => 'drush_devel_generate_content', 'description' => 'Create content.', 'drupal dependencies' => array('devel_generate'), 'arguments' => array( @@ -53,13 +58,14 @@ function devel_generate_drush_command() { 'options' => array( 'kill' => 'Delete all content before generating new content.', 'types' => 'A comma delimited list of content types to create. Defaults to page,article.', - 'feedback' => 'An integer representing interval for insertion rate logging. Defaults to 500', + 'feedback' => 'An integer representing interval for insertion rate logging. Defaults to 1000', 'skip-fields' => 'A comma delimited list of fields to omit when generating random values', 'languages' => 'A comma-separated list of language codes', ), 'aliases' => array('genc'), ); $items['generate-menus'] = array( + 'callback' => 'drush_devel_generate_menus', 'description' => 'Create menus and menu items.', 'drupal dependencies' => array('devel_generate'), // Remove these once devel.module is moved down a directory. http://drupal.org/node/925246 'arguments' => array( @@ -86,7 +92,8 @@ function drush_devel_generate_users($num_users = NULL) { } drush_generate_include_devel(); $roles = drush_get_option('roles') ? explode(',', drush_get_option('roles')) : array(); - devel_create_users($num_users, drush_get_option('kill'), 0, $roles); + $pass = drush_get_option('pass', NULL); + devel_create_users($num_users, drush_get_option('kill'), 0, $roles, $pass); drush_log(t('Generated @number users.', array('@number' => $num_users)), 'success'); } @@ -103,9 +110,14 @@ function drush_devel_generate_terms($vname = NULL, $num_terms = 10) { } drush_generate_include_devel(); - $vocabs[$vocab->vid] = $vocab; - devel_generate_term_data($vocabs, $num_terms, '12', drush_get_option('kill')); - drush_log(dt('Generated @num_terms terms.', array('@num_terms' => $num_terms)), 'success'); + if (drush_get_option('kill')) { + devel_generate_delete_vocabulary_terms($vocab->vid); + drush_log(dt('Deleted existing terms.'), 'success'); + } + $new_terms = devel_generate_terms($num_terms, array($vocab->vid => $vocab), '12'); + if (!empty($new_terms)) { + drush_log(dt("Created the following new terms:\n!terms", array('!terms' => implode("\n", $new_terms))), 'success'); + } } /** @@ -116,8 +128,14 @@ function drush_devel_generate_vocabs($num_vocab = 1) { return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid number of vocabularies: !num.', array('!num' => $num_vocab))); } drush_generate_include_devel(); - devel_generate_vocab_data($num_vocab, '12', drush_get_option('kill')); - drush_log(dt('Generated @num_vocab vocabularies.', array('@num_vocab' => $num_vocab)), 'success'); + if (drush_get_option('kill')) { + devel_generate_delete_vocabularies(); + drush_log(dt('Deleted existing vocabularies.'), 'success'); + } + $new_vocs = devel_generate_vocabs($num_vocab, '12'); + if (!empty($new_vocs)) { + drush_log(dt("Created the following new vocabularies:\n!vocs", array('!vocs' => implode("\n", $new_vocs))), 'success'); + } } /** @@ -152,6 +170,10 @@ function drush_devel_generate_content($num_nodes = NULL, $max_comments = NULL) { $values['values']['num_nodes'] = $num_nodes; $values['values']['max_comments'] = $max_comments; $values['values']['node_types'] = drupal_map_assoc(explode(',', drush_get_option('types', 'page,article'))); + $node_types = array_filter($values['values']['node_types']); + if (!empty($values['values']['kill_content']) && empty($node_types)) { + return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Please provide content type (--types) in which you want to delete the content.')); + } drush_generate_include_devel(); devel_generate_content($values); drush_log(t('Generated @num_nodes nodes, @max_comments comments (or less) per node.', array('@num_nodes' => (int)$num_nodes, '@max_comments' => (int)$max_comments)), 'success'); @@ -179,12 +201,26 @@ function drush_devel_generate_menus($number_menus = 2, $number_links = 50, $max_ $user = $user_one; drupal_save_session(FALSE); - $kill = drush_get_option('kill'); drush_generate_include_devel(); + + // Delete custom menus. + if (drush_get_option('kill')) { + devel_generate_delete_menus(); + drush_log(dt('Deleted existing menus and links.'), 'success'); + } + + // Generate new menus. + $new_menus = devel_generate_menus($number_menus, '12'); + if (!empty($new_menus)) { + drush_log(dt("Created the following new menus:\n!menus", array('!menus' => implode("\n", $new_menus))), 'success'); + } + + // Generate new menu links. $link_types = drupal_map_assoc(array('node', 'front', 'external')); - devel_generate_menu_data($number_menus, array(), $number_links, 12, $link_types, $max_depth, $max_width, $kill); - drush_log(t('Generated @number_menus menus, @number_links links.', array('@number_menus' => (int)$number_menus, '@number_links' => (int)$number_links)), 'success'); + $new_links = devel_generate_links($number_links, $new_menus, '12', $link_types, $max_depth, $max_width); + drush_log(dt('Created !count new menu links.', array('!count' => count($new_links))), 'success'); } + ////////////////////////////////////////////////////////////////////////////// // Helper functions diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.inc b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.inc index 7e757b38..072dafe1 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.inc +++ b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.inc @@ -11,8 +11,10 @@ * The max age of each randomly-generated user, in seconds. * @param $roles * An array of role IDs that the users should receive. + * @param $pass + * A string to be used as common password for all generated users. */ -function devel_create_users($num, $kill, $age = 0, $roles = array()) { +function devel_create_users($num, $kill, $age = 0, $roles = array(), $pass = NULL) { $url = parse_url($GLOBALS['base_url']); if ($kill) { $uids = db_select('users', 'u') @@ -45,8 +47,8 @@ function devel_create_users($num, $kill, $age = 0, $roles = array()) { $edit = array( 'uid' => NULL, 'name' => $name, - 'pass' => NULL, // No password avoids user_hash_password() which is expensive. - 'mail' => $name . '@' . $url['host'], + 'pass' => $pass, + 'mail' => $name . '@' . $url['host'].'.invalid', 'status' => 1, 'created' => REQUEST_TIME - mt_rand(0, $age), 'roles' => drupal_map_assoc($roles), @@ -103,6 +105,7 @@ function devel_create_users($num, $kill, $age = 0, $roles = array()) { // Save the user record with the new picture. $edit = (array) $account; $edit['picture'] = $file; + $edit['pass'] = $pass; // Reassign password as it is replaced with the hashed version in $account user_save($account, $edit); } } @@ -114,8 +117,11 @@ function devel_create_users($num, $kill, $age = 0, $roles = array()) { /** * The main API function for creating content. * - * See devel_generate_content_form() for the supported keys in $form_state['values']. - * Other modules may participate by form_alter() on that form and then handling their data during hook_nodeapi('pre_save') or in own submit handler for the form. + * See devel_generate_content_form() for the supported keys in + * $form_state['values']. + * Other modules may participate by form_alter() on that form and then handling + * their data during hook_node_insert() or in their own submit handler for the + * form. * * @param string $form_state * @return void @@ -205,6 +211,18 @@ function devel_generate_vocabs($records, $maxlength = 12, $types = array('page', return $vocs; } +/** + * Generates taxonomy terms for a list of given vocabularies. + * + * @param $records + * int number of terms to create in total. + * @param $vocabs + * array list of vocabs to populate. + * @param $maxlength + * int maximum length per term. + * @return + * array the list of names of the created terms. + */ function devel_generate_terms($records, $vocabs, $maxlength = 12) { $terms = array(); @@ -255,7 +273,7 @@ function devel_generate_terms($records, $vocabs, $maxlength = 12) { // Populate all core fields on behalf of field.module module_load_include('inc', 'devel_generate', 'devel_generate.fields'); - devel_generate_fields($term, 'term', $term->vocabulary_machine_name); + devel_generate_fields($term, 'taxonomy_term', $term->vocabulary_machine_name); if ($status = taxonomy_term_save($term)) { $max += 1; @@ -289,69 +307,49 @@ function devel_generate_get_terms($vids) { ->fetchCol('tid'); } -function devel_generate_term_data($vocabs, $num_terms, $title_length, $kill) { - if ($kill) { - foreach (devel_generate_get_terms(array_keys($vocabs)) as $tid) { - taxonomy_term_delete($tid); - } - drupal_set_message(t('Deleted existing terms.')); - } - - $new_terms = devel_generate_terms($num_terms, $vocabs, $title_length); - if (!empty($new_terms)) { - drupal_set_message(t('Created the following new terms: !terms', array('!terms' => theme('item_list', array('items' => $new_terms))))); +/** + * Deletes all terms of a vocabulary. + * + * @param $vid + * int a vocabulary vid. + */ +function devel_generate_delete_vocabulary_terms($vid) { + foreach (taxonomy_get_tree($vid) as $term) { + taxonomy_term_delete($term->tid); } } -function devel_generate_vocab_data($num_vocab, $title_length, $kill) { - - if ($kill) { - foreach (taxonomy_get_vocabularies() as $vid => $vocab) { - taxonomy_vocabulary_delete($vid); - } - drupal_set_message(t('Deleted existing vocabularies.')); - } - - $new_vocs = devel_generate_vocabs($num_vocab, $title_length); - if (!empty($new_vocs)) { - drupal_set_message(t('Created the following new vocabularies: !vocs', array('!vocs' => theme('item_list', array('items' => $new_vocs))))); +/** + * Deletes all vocabularies. + */ +function devel_generate_delete_vocabularies() { + foreach (taxonomy_vocabulary_load_multiple(FALSE) as $vid => $vocab) { + taxonomy_vocabulary_delete($vid); } } -function devel_generate_menu_data($num_menus, $existing_menus, $num_links, $title_length, $link_types, $max_depth, $max_width, $kill) { - // Delete menus and menu links. - if ($kill) { - if (module_exists('menu')) { - foreach (menu_get_menus(FALSE) as $menu => $menu_title) { - if (strpos($menu, 'devel-') === 0) { - $menu = menu_load($menu); - menu_delete($menu); - } +/** + * Deletes custom generated menus + */ +function devel_generate_delete_menus() { + if (module_exists('menu')) { + foreach (menu_get_menus(FALSE) as $menu => $menu_title) { + if (strpos($menu, 'devel-') === 0) { + $menu = menu_load($menu); + menu_delete($menu); } } - // Delete menu links generated by devel. - $result = db_select('menu_links', 'm') - ->fields('m', array('mlid')) - ->condition('m.menu_name', 'devel', '<>') - // Look for the serialized version of 'devel' => TRUE. - ->condition('m.options', '%' . db_like('s:5:"devel";b:1') . '%', 'LIKE') - ->execute(); - foreach ($result as $link) { - menu_link_delete($link->mlid); - } - drupal_set_message(t('Deleted existing menus and links.')); } - - // Generate new menus. - $new_menus = devel_generate_menus($num_menus, $title_length); - if (!empty($new_menus)) { - drupal_set_message(t('Created the following new menus: !menus', array('!menus' => theme('item_list', array('items' => $new_menus))))); + // Delete menu links generated by devel. + $result = db_select('menu_links', 'm') + ->fields('m', array('mlid')) + ->condition('m.menu_name', 'devel', '<>') + // Look for the serialized version of 'devel' => TRUE. + ->condition('m.options', '%' . db_like('s:5:"devel";b:1') . '%', 'LIKE') + ->execute(); + foreach ($result as $link) { + menu_link_delete($link->mlid); } - - // Generate new menu links. - $menus = $new_menus + $existing_menus; - $new_links = devel_generate_links($num_links, $menus, $title_length, $link_types, $max_depth, $max_width); - drupal_set_message(t('Created @count new menu links.', array('@count' => count($new_links)))); } /** @@ -366,7 +364,7 @@ function devel_generate_menus($num_menus, $title_length = 12) { for ($i = 1; $i <= $num_menus; $i++) { $menu = array(); - $menu['title'] = devel_generate_word(mt_rand(2, $title_length)); + $menu['title'] = devel_generate_word(mt_rand(2, max(2, $title_length))); $menu['menu_name'] = 'devel-' . drupal_strtolower($menu['title']); $menu['description'] = t('Description of @name', array('@name' => $menu['title'])); menu_save($menu); @@ -394,7 +392,7 @@ function devel_generate_links($num_links, $menus, $title_length, $link_types, $m 'options' => array('devel' => TRUE), 'weight' => mt_rand(-50, 50), 'mlid' => 0, - 'link_title' => devel_generate_word(mt_rand(2, $title_length)), + 'link_title' => devel_generate_word(mt_rand(2, max(2, $title_length))), ); $link['options']['attributes']['title'] = t('Description of @title.', array('@title' => $link['link_title'])); @@ -404,7 +402,7 @@ function devel_generate_links($num_links, $menus, $title_length, $link_types, $m } else { // Otherwise, get a random parent menu depth. - $depth = mt_rand(1, $max_depth - 1); + $depth = mt_rand(1, max(1, $max_depth - 1)); } // Get a random parent link from the proper depth. do { @@ -674,9 +672,19 @@ function devel_generate_content_add_node(&$results) { $users = $results['users']; $node->uid = $users[array_rand($users)]; $type = node_type_get_type($node); - $node->title = $type->has_title ? devel_create_greeking(mt_rand(2, $results['title_length']), TRUE) : ''; $node->revision = mt_rand(0,1); $node->promote = mt_rand(0, 1); + + if ($type->has_title) { + // We should not use the random function if the value is not random + if ($results['title_length'] < 2) { + $node->title = devel_create_greeking(1, TRUE); + } + else { + $node->title = devel_create_greeking(mt_rand(1, $results['title_length']), TRUE); + } + } + // Avoid NOTICE. if (!isset($results['time_range'])) { $results['time_range'] = 0; @@ -686,14 +694,16 @@ function devel_generate_content_add_node(&$results) { $node->created = REQUEST_TIME - mt_rand(0, $results['time_range']); - // A flag to let hook_nodeapi() implementations know that this is a generated node. + // A flag to let hook_node_insert() implementations know that this is a + // generated node. $node->devel_generate = $results; // Populate all core fields on behalf of field.module module_load_include('inc', 'devel_generate', 'devel_generate.fields'); devel_generate_fields($node, 'node', $node->type); - // See devel_generate_nodeapi() for actions that happen before and after this save. + // See devel_generate_node_insert() for actions that happen before and after + // this save. node_save($node); } diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.info b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.info index 877b618a..b45159f5 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.info +++ b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.info @@ -2,13 +2,13 @@ name = Devel generate description = Generate dummy users, nodes, and taxonomy terms. package = Development core = 7.x -dependencies[] = devel tags[] = developer configure = admin/config/development/generate +files[] = devel_generate.test -; Information added by drupal.org packaging script on 2012-06-05 -version = "7.x-1.3" +; Information added by Drupal.org packaging script on 2014-05-01 +version = "7.x-1.5" core = "7.x" project = "devel" -datestamp = "1338940281" +datestamp = "1398963366" diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.module b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.module index 3776c222..11bc2916 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.module +++ b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.module @@ -71,6 +71,14 @@ function devel_generate_users_form() { '#options' => $options, ); + $form['pass'] = array( + '#type' => 'textfield', + '#title' => t('Password to be set'), + '#default_value' => NULL, + '#size' => 32, + '#description' => t('Leave this field empty if you do not need to set a password.'), + ); + $options = array(1 => t('Now')); foreach (array(3600, 86400, 604800, 2592000, 31536000) as $interval) { $options[$interval] = format_interval($interval, 1) . ' ' . t('ago'); @@ -93,10 +101,10 @@ function devel_generate_users_form() { /** * FormAPI submission to generate users. */ -function devel_generate_users_form_submit($form_id, &$form_state) { +function devel_generate_users_form_submit($form, &$form_state) { module_load_include('inc', 'devel_generate'); $values = $form_state['values']; - devel_create_users($values['num'], $values['kill_users'], $values['time_range'], $values['roles']); + devel_create_users($values['num'], $values['kill_users'], $values['time_range'], $values['roles'], $values['pass']); } /** @@ -117,14 +125,15 @@ function devel_generate_content_form() { } else { $types = node_type_get_types(); - $suffix = ''; foreach ($types as $type) { + $options[$type->type] = array( + 'type' => t($type->name), + ); if (module_exists('comment')) { $default = variable_get('comment_' . $type->type, COMMENT_NODE_OPEN); $map = array(t('Hidden'), t('Closed'), t('Open')); - $suffix = '. ' . t('Comments: ') . $map[$default]. ''; + $options[$type->type]['comments'] = ''. $map[$default]. ''; } - $options[$type->type] = t($type->name) . $suffix; } } // we cannot currently generate valid polls. @@ -135,11 +144,18 @@ function devel_generate_content_form() { return; } + $header = array( + 'type' => t('Content type'), + ); + if (module_exists('comment')) { + $header['comments'] = t('Comments'); + } + $form['node_types'] = array( - '#type' => 'checkboxes', - '#title' => t('Content types'), + '#type' => 'tableselect', + '#header' => $header, '#options' => $options, - '#default_value' => array_keys($options), + '#required' => TRUE, ); if (module_exists('checkall')) $form['node_types']['#checkall'] = TRUE; $form['kill_content'] = array( @@ -218,12 +234,21 @@ function devel_generate_content_form() { return $form; } +/** + * FormAPI validation before generate nodes. + */ +function devel_generate_content_form_validate($form, &$form_state) { + form_set_value($form['node_types'], array_filter($form_state['values']['node_types']) , $form_state); + if (!empty($form_state['values']['kill_content']) && empty($form_state['values']['node_types'])) { + form_set_error('', t('Please select at least one content type to delete the content.')); + } +} + /** * FormAPI submission to generate nodes. */ -function devel_generate_content_form_submit($form_id, &$form_state) { +function devel_generate_content_form_submit($form, &$form_state) { module_load_include('inc', 'devel_generate', 'devel_generate'); - $form_state['values']['node_types'] = array_filter($form_state['values']['node_types']); if ($form_state['values']['num_nodes'] <= 50 && $form_state['values']['max_comments'] <= 10) { module_load_include('inc', 'devel_generate'); devel_generate_content($form_state); @@ -305,18 +330,36 @@ function devel_generate_vocab_form() { /** * FormAPI submission to generate taxonomy terms. */ -function devel_generate_term_form_submit($form_id, &$form_state) { +function devel_generate_term_form_submit($form, &$form_state) { + $values = $form_state['values']; module_load_include('inc', 'devel_generate'); - $vocabs = taxonomy_vocabulary_load_multiple($form_state['values']['vids']); - devel_generate_term_data($vocabs, $form_state['values']['num_terms'], $form_state['values']['title_length'], $form_state['values']['kill_taxonomy']); + if ($values['kill_taxonomy']) { + foreach ($values['vids'] as $vid) { + devel_generate_delete_vocabulary_terms($vid); + } + drupal_set_message(t('Deleted existing terms.')); + } + $vocabs = taxonomy_vocabulary_load_multiple($values['vids']); + $new_terms = devel_generate_terms($values['num_terms'], $vocabs, $values['title_length']); + if (!empty($new_terms)) { + drupal_set_message(t('Created the following new terms: !terms', array('!terms' => implode(', ', $new_terms)))); + } } /** * FormAPI submission to generate taxonomy vocabularies. */ -function devel_generate_vocab_form_submit($form_id, &$form_state) { +function devel_generate_vocab_form_submit($form, &$form_state) { + $values = $form_state['values']; module_load_include('inc', 'devel_generate'); - devel_generate_vocab_data($form_state['values']['num_vocabs'], $form_state['values']['title_length'], $form_state['values']['kill_taxonomy']); + if ($values['kill_taxonomy']) { + devel_generate_delete_vocabularies(); + drupal_set_message(t('Deleted existing vocabularies.')); + } + $new_vocs = devel_generate_vocabs($values['num_vocabs'], $values['title_length']); + if (!empty($new_vocs)) { + drupal_set_message(t('Created the following new vocabularies: !vocs', array('!vocs' => implode(', ', $new_vocs)))); + } } /** @@ -407,6 +450,7 @@ function devel_generate_menu_form() { $form['title_length'] = array( '#type' => 'textfield', '#title' => t('Maximum number of characters in menu and menu link names'), + '#description' => t("The minimum length is 2."), '#default_value' => 12, '#size' => 10, '#required' => TRUE, @@ -453,11 +497,26 @@ function devel_generate_menu_form() { /** * FormAPI submission to generate menus. */ -function devel_generate_menu_form_submit($form_id, &$form_state) { +function devel_generate_menu_form_submit($form, &$form_state) { // If the create new menus checkbox is off, set the number of new menus to 0. if (!isset($form_state['values']['existing_menus']['__new-menu__']) || !$form_state['values']['existing_menus']['__new-menu__']) { $form_state['values']['num_menus'] = 0; } module_load_include('inc', 'devel_generate'); - devel_generate_menu_data($form_state['values']['num_menus'], $form_state['values']['existing_menus'], $form_state['values']['num_links'], $form_state['values']['title_length'], $form_state['values']['link_types'], $form_state['values']['max_depth'], $form_state['values']['max_width'], $form_state['values']['kill']); + // Delete custom menus. + if ($form_state['values']['kill']) { + devel_generate_delete_menus(); + drupal_set_message(t('Deleted existing menus and links.')); + } + + // Generate new menus. + $new_menus = devel_generate_menus($form_state['values']['num_menus'], $form_state['values']['title_length']); + if (!empty($new_menus)) { + drupal_set_message(t('Created the following new menus: !menus', array('!menus' => implode(', ', $new_menus)))); + } + + // Generate new menu links. + $menus = $new_menus + $form_state['values']['existing_menus']; + $new_links = devel_generate_links($form_state['values']['num_links'], $menus, $form_state['values']['title_length'], $form_state['values']['link_types'], $form_state['values']['max_depth'], $form_state['values']['max_width']); + drupal_set_message(t('Created @count new menu links.', array('@count' => count($new_links)))); } diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.test b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.test new file mode 100644 index 00000000..c649f024 --- /dev/null +++ b/sites/all/modules/contrib/dev/devel/devel_generate/devel_generate.test @@ -0,0 +1,108 @@ + t('Devel Generate'), + 'description' => t('Tests the logic to generate data.'), + 'group' => t('Devel'), + ); + } + + /** + * Prepares the testing environment + */ + function setUp() { + parent::setUp(array('devel', 'devel_generate', 'taxonomy', 'menu', 'comment')); + + // Create Basic page and Article node types. + if ($this->profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic Page')); + } + } + + /** + * Tests generate commands + */ + public function testGenerate() { + $user = $this->drupalCreateUser(array( + 'administer taxonomy', + 'administer menu', + 'administer nodes', + )); + $this->drupalLogin($user); + + // Generate taxonomy vocabularies. + $edit = array( + 'num_vocabs' => 5, + 'title_length' => 12, + 'kill_taxonomy' => 1, + ); + $this->drupalPost('admin/config/development/generate/vocabs', + $edit, t('Generate')); + $this->assertText(t('Deleted existing vocabularies.')); + $this->assertText(t('Created the following new vocabularies:')); + + // Generate taxonomy terms. + $form = devel_generate_term_form(); + $vids = array_keys($form['vids']['#options']); + $edit = array( + 'vids[]' => $vids, + 'num_terms' => 5, + 'title_length' => 12, + 'kill_taxonomy' => 1, + ); + $this->drupalPost('admin/config/development/generate/taxonomy', + $edit, t('Generate')); + $this->assertText(t('Deleted existing terms.')); + $this->assertText(t('Created the following new terms: ')); + + // Generate menus. + $edit = array( + 'existing_menus[__new-menu__]' => 1, + 'num_menus' => 2, + 'num_links' => 50, + 'title_length' => 12, + 'link_types[node]' => 1, + 'link_types[front]' => 1, + 'link_types[external]' => 1, + 'max_depth' => 4, + 'max_width' => 6, + 'kill' => 1, + ); + $this->drupalPost('admin/config/development/generate/menu', + $edit, t('Generate')); + $this->assertText(t('Deleted existing menus and links.')); + $this->assertText(t('Created the following new menus:')); + $this->assertText(t('Created 50 new menu links.')); + + // Generate content. + // First we create a node in order to test the Delete content checkbox. + $this->drupalCreateNode(array()); + + // Now submit the generate content form. + $edit = array( + 'node_types[page]' => 1, + 'kill_content' => 1, + 'num_nodes' => 2, + 'time_range' => 604800, + 'max_comments' => 3, + 'title_length' => 4, + ); + $this->drupalPost('admin/config/development/generate/content', $edit, t('Generate')); + $this->assertText(t('Deleted 1 nodes.')); + $this->assertText(t('Finished creating 2 nodes')); + } +} diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/file.devel_generate.inc b/sites/all/modules/contrib/dev/devel/devel_generate/file.devel_generate.inc index 84ed5edf..c4e76eac 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/file.devel_generate.inc +++ b/sites/all/modules/contrib/dev/devel/devel_generate/file.devel_generate.inc @@ -18,7 +18,7 @@ function _file_devel_generate($object, $field, $instance, $bundle) { $source->uri = $path; $source->uid = 1; // TODO: randomize? use case specific. $source->filemime = 'text/plain'; - $source->filename = array_pop(explode("//", $path)); + $source->filename = basename($path); $destination_dir = $field['settings']['uri_scheme'] . '://' . $instance['settings']['file_directory']; file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY); $destination = $destination_dir . '/' . basename($path); diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/image.devel_generate.inc b/sites/all/modules/contrib/dev/devel/devel_generate/image.devel_generate.inc index df1705ce..df4ff588 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/image.devel_generate.inc +++ b/sites/all/modules/contrib/dev/devel/devel_generate/image.devel_generate.inc @@ -23,7 +23,7 @@ function _image_devel_generate($object, $field, $instance, $bundle) { $min_resolution = empty($instance['settings']['min_resolution']) ? '100x100' : $instance['settings']['min_resolution']; $max_resolution = empty($instance['settings']['max_resolution']) ? '600x600' : $instance['settings']['max_resolution']; - $extensions = array_intersect(explode(' ', $instance['settings']['file_extensions']), array('png', 'jpg')); + $extensions = array_intersect(explode(' ', $instance['settings']['file_extensions']), array('png', 'gif', 'jpg', 'jpeg')); $extension = array_rand(drupal_map_assoc($extensions)); // Generate a max of 5 different images. @@ -33,7 +33,7 @@ function _image_devel_generate($object, $field, $instance, $bundle) { $source->uri = $path; $source->uid = 1; // TODO: randomize? Use case specific. $source->filemime = 'image/' . pathinfo($path, PATHINFO_EXTENSION); - $source->filename = array_pop(explode("//", $path)); + $source->filename = basename($path); $destination_dir = $field['settings']['uri_scheme'] . '://' . $instance['settings']['file_directory']; file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY); $destination = $destination_dir . '/' . basename($path); @@ -89,8 +89,6 @@ function devel_generate_image($extension = 'png', $min_resolution, $max_resoluti $save_function = 'image'. ($extension == 'jpg' ? 'jpeg' : $extension); $save_function($im, drupal_realpath($destination)); - - $images[$extension][$min_resolution][$max_resolution][$destination] = $destination; } return $destination; } diff --git a/sites/all/modules/contrib/dev/devel/devel_generate/text.devel_generate.inc b/sites/all/modules/contrib/dev/devel/devel_generate/text.devel_generate.inc index 1f4691d2..c63a92ed 100644 --- a/sites/all/modules/contrib/dev/devel/devel_generate/text.devel_generate.inc +++ b/sites/all/modules/contrib/dev/devel/devel_generate/text.devel_generate.inc @@ -19,7 +19,7 @@ function _text_devel_generate($object, $field, $instance, $bundle) { $format = filter_fallback_format(); } - if ($instance['widget']['type'] != 'text_textfield') { + if (empty($field['settings']['max_length'])) { // Textarea handling $object_field['value'] = devel_create_content($format); if ($instance['widget']['type'] == 'text_textarea_with_summary' && !empty($instance['display_summary'])) { @@ -28,11 +28,7 @@ function _text_devel_generate($object, $field, $instance, $bundle) { } else { // Textfield handling. - // Generate a value that respects max_length. - if (empty($field['settings']['max_length'])) { - $field['settings']['max_length'] = 12; - } - $object_field['value'] = user_password($field['settings']['max_length']); + $object_field['value'] = substr(devel_create_greeking(mt_rand(1, $field['settings']['max_length'] / 6), FALSE), 0, $field['settings']['max_length']); } $object_field['format'] = $format; return $object_field; diff --git a/sites/all/modules/contrib/dev/devel/devel_krumo_path.js b/sites/all/modules/contrib/dev/devel/devel_krumo_path.js index fa01f771..d4a578dd 100644 --- a/sites/all/modules/contrib/dev/devel/devel_krumo_path.js +++ b/sites/all/modules/contrib/dev/devel/devel_krumo_path.js @@ -7,7 +7,7 @@ Drupal.behaviors.devel = { attach: function (context, settings) { // Add hint to footnote - $('.krumo-footnote .krumo-call').before('
$bee){
- if (is_object($bee)) {
+ // skip closures set as properties
+ if (is_object($bee) && !($bee instanceof Closure)) {
unset($hive[$i]->$_recursion_marker);
// DEVEL: changed 'else' to 'elseif' below
} elseif (is_array($bee)) {
diff --git a/sites/all/modules/contrib/dev/devel/runtests.sh b/sites/all/modules/contrib/dev/devel/runtests.sh
new file mode 100644
index 00000000..4eb200a1
--- /dev/null
+++ b/sites/all/modules/contrib/dev/devel/runtests.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env sh
+
+# This script will run phpunit-based test classes using Drush's
+# test framework. First, the Drush executable is located, and
+# then phpunit is invoked, passing in drush_testcase.inc as
+# the bootstrap file.
+#
+# Any parameters that may be passed to phpunit may also be used
+# with runtests.sh.
+
+DRUSH_PATH="`which drush`"
+DRUSH_DIRNAME="`dirname -- "$DRUSH_PATH"`"
+
+# if [ $# = 0 ] ; then
+# phpunit --bootstrap="$DRUSH_DIRNAME/tests/drush_testcase.inc" .
+# else
+# phpunit --bootstrap="$DRUSH_DIRNAME/tests/drush_testcase.inc" "$@"
+# fi
+
+#Instead, hard code target file so we don't find a simpletest file at
+# /lib/Drupal/devel_generate/Tests/DevelGenerateTest.php.
+phpunit --bootstrap="$DRUSH_DIRNAME/tests/drush_testcase.inc" develDrushTest.php
diff --git a/sites/all/modules/contrib/dev/libraries/CHANGELOG.txt b/sites/all/modules/contrib/dev/libraries/CHANGELOG.txt
index 4738559b..e3c94119 100644
--- a/sites/all/modules/contrib/dev/libraries/CHANGELOG.txt
+++ b/sites/all/modules/contrib/dev/libraries/CHANGELOG.txt
@@ -1,4 +1,22 @@
+Libraries 7.x-2.2, 2014-02-09
+-----------------------------
+#2046919 by tstoeckler: Clarify 'version' docs.
+#1946110 by munroe_richard: Allow uppercase letters as library machine names.
+#1953260 by tstoeckler: Improve documentation of libraries_get_version().
+#1855918 by tstoeckler: Make integration file loading backwards-compatible.
+#1876124 by tstoeckler: Fix integration files for themes.
+#1876124 by tstoeckler: Add tests for theme-provided library information.
+#1876124 by tstoeckler: Prepare for adding a test theme.
+#1876124 by tstoeckler | whastings, fubhy: Fix hook_libraries_info() for themes.
+#2015721 by tstoeckler, CaptainHook: Protect against files overriding local variables.
+#2046919 by tstoeckler: Improve documentation around 'version callback'.
+#1844272 by tstoeckler, jweowu: Fix typos in libraries.api.php.
+#1938638 by tstoeckler: Prevent weird PHP notice on update.
+#1329388 by RobLoach, tstoeckler: Clear static caches in libraries_flush_caches().
+#1855918 by rbayliss: Load integration files after library files.
+#1938638 by Pol: Fix typo in libraries.api.php.
+
Libraries 7.x-2.1, 2013-03-09
-----------------------------
#1937446 by Pol, tstoeckler: Add a 'pre-dependencies-load' callback group.
@@ -13,7 +31,6 @@ Libraries 7.x-2.0, 2012-07-29
#1578618 by iamEAP: Fixed Fatal cache flush failure on major version upgrades.
#1449346 by tstoeckler, sun: Clean-up libraries.test
-
Libraries 7.x-2.0-alpha2, 2011-12-15
------------------------------------
#1299076 by tstoeckler: Improve testing of JS, CSS, and PHP files.
@@ -23,7 +40,6 @@ Libraries 7.x-2.0-alpha2, 2011-12-15
#1321372 by Rob Loach: Provide a 'post-load' callback group.
#1205854 by tstoeckler, sun: Test library caching.
-
Libraries 7.x-2.0-alpha1, 2011-10-01
------------------------------------
#1268342 by tstoeckler: Clean up drush libraries-list command.
@@ -65,20 +81,20 @@ by sun: Fixed testbot breaks upon .info file without .module file.
Libraries 7.x-1.x, xxxx-xx-xx
-----------------------------
-
Libraries 7.x-1.0, 2010-01-27
-----------------------------
#743522 by sun: Ported to D7.
+Libraries 6.x-1.x, xxxx-xx-xx
+-----------------------------
+
Libraries 6.x-1.0, 2010-01-27
-----------------------------
#1028744 by tstoeckler: Code clean-up.
#496732 by tstoeckler, robphillips: Allow placing libraries in root directory.
-
-Libraries 6.x-1.0-ALPHA1, 2009-12-30
+Libraries 6.x-1.0-alpha1, 2009-12-30
------------------------------------
#480440 by markus_petrux: Fixed base_path() not applied to default library path.
#320562 by sun: Added basic functions.
-
diff --git a/sites/all/modules/contrib/dev/libraries/libraries.api.php b/sites/all/modules/contrib/dev/libraries/libraries.api.php
index 9ae1b32f..8ac31115 100644
--- a/sites/all/modules/contrib/dev/libraries/libraries.api.php
+++ b/sites/all/modules/contrib/dev/libraries/libraries.api.php
@@ -30,7 +30,9 @@
* changes of implementing modules and to support different versions of a
* library simultaneously (though only one version can be installed per
* site). A valid use-case is an external library whose version cannot be
- * determined programatically.
+ * determined programmatically. Either 'version' or 'version callback' (or
+ * 'version arguments' in case libraries_get_version() is being used as a
+ * version callback) must be declared.
* - version callback: (optional) The name of a function that detects and
* returns the full version string of the library. The first argument is
* always $library, an array containing all library information as described
@@ -38,18 +40,22 @@
* arguments, either as a single $options parameter or as multiple
* parameters, which correspond to the two ways to specify the argument
* values (see 'version arguments'). Defaults to libraries_get_version().
- * - version arguments: A list of arguments to pass to the version callback.
- * Version arguments can be declared either as an associative array whose
- * keys are the argument names or as an indexed array without specifying
- * keys. If declared as an associative array, the arguments get passed to
- * the version callback as a single $options parameter whose keys are the
- * argument names (i.e. $options is identical to the specified array). If
- * declared as an indexed array, the array values get passed to the version
- * callback as seperate arguments in the order they were declared. The
- * default version callback libraries_get_version() expects a single,
- * associative array with named keys:
- * - file: The filename to parse for the version, relative to the library
- * path. For example: 'docs/changelog.txt'.
+ * Unless 'version' is declared or libraries_get_version() is being used as
+ * a version callback, 'version callback' must be declared. In the latter
+ * case, however, 'version arguments' must be declared in the specified way.
+ * - version arguments: (optional) A list of arguments to pass to the version
+ * callback. Version arguments can be declared either as an associative
+ * array whose keys are the argument names or as an indexed array without
+ * specifying keys. If declared as an associative array, the arguments get
+ * passed to the version callback as a single $options parameter whose keys
+ * are the argument names (i.e. $options is identical to the specified
+ * array). If declared as an indexed array, the array values get passed to
+ * the version callback as separate arguments in the order they were
+ * declared. The default version callback libraries_get_version() expects a
+ * single, associative array with named keys:
+ * - file: The filename to parse for the version, relative to the path
+ * speficied as the 'library path' property (see above). For example:
+ * 'docs/changelog.txt'.
* - pattern: A string containing a regular expression (PCRE) to match the
* library version. For example: '@version\s+([0-9a-zA-Z\.-]+)@'. Note
* that the returned version is not the match of the entire pattern (i.e.
@@ -60,6 +66,10 @@
* - cols: (optional) The maximum number of characters per line to take into
* account. Defaults to 200. In case of minified or compressed files, this
* prevents reading the entire file into memory.
+ * Defaults to an empty array. 'version arguments' must be specified unless
+ * 'version' is declared or the specified 'version callback' does not
+ * require any arguments. The latter might be the case with a
+ * library-specific version callback, for example.
* - files: An associative array of library files to load. Supported keys are:
* - js: A list of JavaScript files to load, using the same syntax as Drupal
* core's hook_library().
@@ -100,10 +110,10 @@
* available or not. The first argument is always $library, an array
* containing all library information as described here. The second
* argument is always a string containing the variant name. There are two
- * ways to declare the variant callback's additinal arguments, either as a
+ * ways to declare the variant callback's additional arguments, either as a
* single $options parameter or as multiple parameters, which correspond
* to the two ways to specify the argument values (see 'variant
- * arguments'). If ommitted, the variant is expected to always be
+ * arguments'). If omitted, the variant is expected to always be
* available.
* - variant arguments: A list of arguments to pass to the variant callback.
* Variant arguments can be declared either as an associative array whose
@@ -112,7 +122,7 @@
* the variant callback as a single $options parameter whose keys are the
* argument names (i.e. $options is identical to the specified array). If
* declared as an indexed array, the array values get passed to the
- * variant callback as seperate arguments in the order they were declared.
+ * variant callback as separate arguments in the order they were declared.
* Variants can be version-specific (see 'versions').
* - versions: (optional) An associative array of supported library versions.
* Naturally, libraries evolve over time and so do their APIs. In case a
@@ -146,9 +156,23 @@
* Valid callback groups are:
* - info: Callbacks registered in this group are applied after the library
* information has been retrieved via hook_libraries_info() or info files.
+ * At this point the following additional information is available:
+ * - $library['info type']: How the library information was obtained. Can
+ * be 'info file', 'module', or 'theme', depending on whether the
+ * library information was obtained from an info file, an enabled module
+ * or an enabled theme, respectively.
+ * Additionally, one of the following three keys is available, depending
+ * on the value of $library['info type'].
+ * - $library['info file']: In case the library information was obtained
+ * from an info file, the URI of the info file.
+ * - $library['module']: In case the library was obtained from an enabled
+ * module, the name of the providing module.
+ * - $library['theme']: In case the library was obtained from an enabled
+ * theme, the name of the providing theme.
* - pre-detect: Callbacks registered in this group are applied after the
* library path has been determined and before the version callback is
- * invoked. At this point the following additional information is available:
+ * invoked. At this point the following additional information is
+ * available:
* - $library['library path']: The path on the file system to the library.
* - post-detect: Callbacks registered in this group are applied after the
* library has been successfully detected. At this point the library
@@ -299,7 +323,7 @@ function hook_libraries_info() {
'mymodule_example_libraries_postdetect_callback',
),
// Called before the library's dependencies are loaded.
- 'pre-dependencie-load' => array(
+ 'pre-dependencies-load' => array(
'mymodule_example_libraries_pre_dependencies_load_callback',
),
// Called before the library is loaded.
diff --git a/sites/all/modules/contrib/dev/libraries/libraries.info b/sites/all/modules/contrib/dev/libraries/libraries.info
index d4b2af3f..136d323b 100644
--- a/sites/all/modules/contrib/dev/libraries/libraries.info
+++ b/sites/all/modules/contrib/dev/libraries/libraries.info
@@ -1,11 +1,13 @@
name = Libraries
description = Allows version-dependent and shared usage of external libraries.
core = 7.x
+; We use hook_system_theme_info() which was added in Drupal 7.11
+dependencies[] = system (>=7.11)
files[] = tests/libraries.test
-; Information added by drupal.org packaging script on 2013-03-09
-version = "7.x-2.1"
+; Information added by Drupal.org packaging script on 2014-02-09
+version = "7.x-2.2"
core = "7.x"
project = "libraries"
-datestamp = "1362848412"
+datestamp = "1391965716"
diff --git a/sites/all/modules/contrib/dev/libraries/libraries.module b/sites/all/modules/contrib/dev/libraries/libraries.module
index 687fa290..0448476d 100644
--- a/sites/all/modules/contrib/dev/libraries/libraries.module
+++ b/sites/all/modules/contrib/dev/libraries/libraries.module
@@ -9,6 +9,14 @@
* Implements hook_flush_caches().
*/
function libraries_flush_caches() {
+ // Clear static caches.
+ // We don't clear the 'libraries_load' static cache, because that could result
+ // in libraries that had been loaded before the cache flushing to be loaded
+ // again afterwards.
+ foreach (array('libraries_get_path', 'libraries_info') as $name) {
+ drupal_static_reset($name);
+ }
+
// @todo When upgrading from 1.x, update.php attempts to flush caches before
// the cache table has been created.
// @see http://drupal.org/node/1477932
@@ -133,7 +141,7 @@ function libraries_scan_info_files() {
$files = array();
foreach ($directories as $dir) {
if (file_exists($dir)) {
- $files = array_merge($files, file_scan_directory($dir, '@^[a-z0-9._-]+\.libraries\.info$@', array(
+ $files = array_merge($files, file_scan_directory($dir, '@^[A-Za-z0-9._-]+\.libraries\.info$@', array(
'key' => 'name',
'recurse' => FALSE,
)));
@@ -158,6 +166,13 @@ function libraries_scan_info_files() {
* An array of library information, passed by reference.
*/
function libraries_invoke($group, &$library) {
+ // When introducing new callback groups in newer versions, stale cached
+ // library information somehow reaches this point during the database update
+ // before clearing the library cache.
+ if (empty($library['callbacks'][$group])) {
+ return;
+ }
+
foreach ($library['callbacks'][$group] as $callback) {
libraries_traverse_library($library, $callback);
}
@@ -333,27 +348,31 @@ function &libraries_info($name = NULL) {
if (!isset($libraries)) {
$libraries = array();
- // Gather information from hook_libraries_info().
+
+ // Gather information from hook_libraries_info() in enabled modules.
foreach (module_implements('libraries_info') as $module) {
foreach (module_invoke($module, 'libraries_info') as $machine_name => $properties) {
+ $properties['info type'] = 'module';
$properties['module'] = $module;
$libraries[$machine_name] = $properties;
}
}
+
// Gather information from hook_libraries_info() in enabled themes.
- // @see drupal_alter()
- global $theme, $base_theme_info;
- if (isset($theme)) {
- $theme_keys = array();
- foreach ($base_theme_info as $base) {
- $theme_keys[] = $base->name;
- }
- $theme_keys[] = $theme;
- foreach ($theme_keys as $theme_key) {
- $function = $theme_key . '_' . 'libraries_info';
+ $themes = array();
+ foreach (list_themes() as $theme_name => $theme_info) {
+ if ($theme_info->status && file_exists(drupal_get_path('theme', $theme_name) . '/template.php')) {
+ // Collect a list of viable themes for re-use when calling the alter
+ // hook.
+ $themes[] = $theme_name;
+
+ include_once drupal_get_path('theme', $theme_name) . '/template.php';
+
+ $function = $theme_name . '_libraries_info';
if (function_exists($function)) {
foreach ($function() as $machine_name => $properties) {
- $properties['theme'] = $theme_key;
+ $properties['info type'] = 'theme';
+ $properties['theme'] = $theme_name;
$libraries[$machine_name] = $properties;
}
}
@@ -364,6 +383,7 @@ function &libraries_info($name = NULL) {
// .info files override module definitions.
foreach (libraries_scan_info_files() as $machine_name => $file) {
$properties = drupal_parse_info_file($file->uri);
+ $properties['info type'] = 'info file';
$properties['info file'] = $file->uri;
$libraries[$machine_name] = $properties;
}
@@ -373,8 +393,20 @@ function &libraries_info($name = NULL) {
libraries_info_defaults($properties, $machine_name);
}
- // Allow modules to alter the registered libraries.
- drupal_alter('libraries_info', $libraries);
+ // Allow enabled modules and themes to alter the registered libraries.
+ // drupal_alter() only takes the currently active theme into account, not
+ // all enabled themes.
+ foreach (module_implements('libraries_info_alter') as $module) {
+ $function = $module . '_libraries_info_alter';
+ $function($libraries);
+ }
+ foreach ($themes as $theme) {
+ $function = $theme . '_libraries_info_alter';
+ // The template.php file was included above.
+ if (function_exists($function)) {
+ $function($libraries);
+ }
+ }
// Invoke callbacks in the 'info' group.
foreach ($libraries as &$properties) {
@@ -418,6 +450,8 @@ function libraries_info_defaults(&$library, $name) {
'versions' => array(),
'integration files' => array(),
'callbacks' => array(),
+ // @todo Remove in 7.x-3.x
+ 'post-load integration files' => FALSE,
);
$library['callbacks'] += array(
'info' => array(),
@@ -461,9 +495,11 @@ function libraries_detect($name) {
// Re-use the statically cached value of libraries_info() to save memory.
$library = &libraries_info($name);
+ // Exit early if the library was not found.
if ($library === FALSE) {
return $library;
}
+
// If 'installed' is set, library detection ran already.
if (isset($library['installed'])) {
return $library;
@@ -600,6 +636,12 @@ function libraries_load($name, $variant = NULL) {
cache_set($name, $library, 'cache_libraries');
}
+ // Exit early if the library was not found.
+ if ($library === FALSE) {
+ $loaded[$name] = $library;
+ return $loaded[$name];
+ }
+
// If a variant was specified, override the top-level properties with the
// variant properties.
if (isset($variant)) {
@@ -652,13 +694,30 @@ function libraries_load($name, $variant = NULL) {
*/
function libraries_load_files($library) {
// Load integration files.
- if (!empty($library['integration files'])) {
- foreach ($library['integration files'] as $module => $files) {
- libraries_load_files(array(
- 'files' => $files,
- 'path' => '',
- 'library path' => drupal_get_path('module', $module),
- ));
+ if (!$library['post-load integration files'] && !empty($library['integration files'])) {
+ $enabled_themes = array();
+ foreach (list_themes() as $theme_name => $theme) {
+ if ($theme->status) {
+ $enabled_themes[] = $theme_name;
+ }
+ }
+ foreach ($library['integration files'] as $provider => $files) {
+ if (module_exists($provider)) {
+ libraries_load_files(array(
+ 'files' => $files,
+ 'path' => '',
+ 'library path' => drupal_get_path('module', $provider),
+ 'post-load integration files' => FALSE,
+ ));
+ }
+ elseif (in_array($provider, $enabled_themes)) {
+ libraries_load_files(array(
+ 'files' => $files,
+ 'path' => '',
+ 'library path' => drupal_get_path('theme', $provider),
+ 'post-load integration files' => FALSE,
+ ));
+ }
}
}
@@ -706,15 +765,60 @@ function libraries_load_files($library) {
foreach ($library['files']['php'] as $file => $array) {
$file_path = DRUPAL_ROOT . '/' . $path . '/' . $file;
if (file_exists($file_path)) {
- require_once $file_path;
+ _libraries_require_once($file_path);
$count++;
}
}
}
+ // Load integration files.
+ if ($library['post-load integration files'] && !empty($library['integration files'])) {
+ $enabled_themes = array();
+ foreach (list_themes() as $theme_name => $theme) {
+ if ($theme->status) {
+ $enabled_themes[] = $theme_name;
+ }
+ }
+ foreach ($library['integration files'] as $provider => $files) {
+ if (module_exists($provider)) {
+ libraries_load_files(array(
+ 'files' => $files,
+ 'path' => '',
+ 'library path' => drupal_get_path('module', $provider),
+ 'post-load integration files' => FALSE,
+ ));
+ }
+ elseif (in_array($provider, $enabled_themes)) {
+ libraries_load_files(array(
+ 'files' => $files,
+ 'path' => '',
+ 'library path' => drupal_get_path('theme', $provider),
+ 'post-load integration files' => FALSE,
+ ));
+ }
+ }
+ }
+
return $count;
}
+/**
+ * Wrapper function for require_once.
+ *
+ * A library file could set a $path variable in file scope. Requiring such a
+ * file directly in libraries_load_files() would lead to the local $path
+ * variable being overridden after the require_once statement. This would
+ * break loading further files. Therefore we use this trivial wrapper which has
+ * no local state that can be tampered with.
+ *
+ * @param $file_path
+ * The file path of the file to require.
+ */
+function _libraries_require_once($file_path) {
+ require_once $file_path;
+}
+
+
/**
* Gets the version information from an arbitrary library.
*
diff --git a/sites/all/modules/contrib/dev/libraries/tests/example/example_1.css b/sites/all/modules/contrib/dev/libraries/tests/example/example_1.css
deleted file mode 100644
index a732bda5..00000000
--- a/sites/all/modules/contrib/dev/libraries/tests/example/example_1.css
+++ /dev/null
@@ -1,11 +0,0 @@
-
-/**
- * @file
- * Test CSS file for Libraries loading.
- *
- * Color the 'libraries-test-css' div red. See README.txt for more information.
- */
-
-.libraries-test-css {
- color: red;
-}
diff --git a/sites/all/modules/contrib/dev/libraries/tests/example/example_2.css b/sites/all/modules/contrib/dev/libraries/tests/example/example_2.css
deleted file mode 100644
index c8f92899..00000000
--- a/sites/all/modules/contrib/dev/libraries/tests/example/example_2.css
+++ /dev/null
@@ -1,11 +0,0 @@
-
-/**
- * @file
- * Test CSS file for Libraries loading.
- *
- * Color the 'libraries-test-css' div green. See README.txt for more information.
- */
-
-.libraries-test-css {
- color: green;
-}
diff --git a/sites/all/modules/contrib/dev/libraries/tests/example/example_3.css b/sites/all/modules/contrib/dev/libraries/tests/example/example_3.css
deleted file mode 100644
index ffef054a..00000000
--- a/sites/all/modules/contrib/dev/libraries/tests/example/example_3.css
+++ /dev/null
@@ -1,11 +0,0 @@
-
-/**
- * @file
- * Test CSS file for Libraries loading.
- *
- * Color the 'libraries-test-css' div orange. See README.txt for more information.
- */
-
-.libraries-test-css {
- color: orange;
-}
diff --git a/sites/all/modules/contrib/dev/libraries/tests/example/example_4.css b/sites/all/modules/contrib/dev/libraries/tests/example/example_4.css
deleted file mode 100644
index 5030a614..00000000
--- a/sites/all/modules/contrib/dev/libraries/tests/example/example_4.css
+++ /dev/null
@@ -1,11 +0,0 @@
-
-/**
- * @file
- * Test CSS file for Libraries loading.
- *
- * Color the 'libraries-test-css' div blue. See README.txt for more information.
- */
-
-.libraries-test-css {
- color: blue;
-}
diff --git a/sites/all/modules/contrib/dev/libraries/tests/libraries.test b/sites/all/modules/contrib/dev/libraries/tests/libraries.test
index 1770162e..41926318 100644
--- a/sites/all/modules/contrib/dev/libraries/tests/libraries.test
+++ b/sites/all/modules/contrib/dev/libraries/tests/libraries.test
@@ -72,7 +72,8 @@ class LibrariesTestCase extends DrupalWebTestCase {
}
function setUp() {
- parent::setUp('libraries', 'libraries_test');
+ parent::setUp('libraries', 'libraries_test_module');
+ theme_enable(array('libraries_test_theme'));
}
/**
@@ -131,6 +132,7 @@ class LibrariesTestCase extends DrupalWebTestCase {
// FALSE for missing or incompatible dependencies.
$library['installed'] = TRUE;
libraries_detect_dependencies($library);
+ $this->verbose('Library:' . var_export($library, TRUE) . '
');
$this->assertTrue($library['installed'], "libraries_detect_dependencies() detects compatible version string: '$version_string' is compatible with '$version'");
}
foreach ($incompatible as $version_string) {
@@ -138,6 +140,7 @@ class LibrariesTestCase extends DrupalWebTestCase {
$library['installed'] = TRUE;
unset($library['error'], $library['error message']);
libraries_detect_dependencies($library);
+ $this->verbose('Library:' . var_export($library, TRUE) . '
');
$this->assertEqual($library['error'], 'incompatible dependency', "libraries_detect_dependencies() detects incompatible version strings: '$version_string' is incompatible with '$version'");
}
// Instead of repeating this assertion for each version string, we just
@@ -157,7 +160,7 @@ class LibrariesTestCase extends DrupalWebTestCase {
*/
function testLibrariesScanInfoFiles() {
$expected = array('example_info_file' => (object) array(
- 'uri' => drupal_get_path('module', 'libraries') . '/tests/example/example_info_file.libraries.info',
+ 'uri' => drupal_get_path('module', 'libraries') . '/tests/libraries/example_info_file.libraries.info',
'filename' => 'example_info_file.libraries.info',
'name' => 'example_info_file.libraries',
));
@@ -169,17 +172,33 @@ class LibrariesTestCase extends DrupalWebTestCase {
* Tests libraries_info().
*/
function testLibrariesInfo() {
+ // Test that modules can provide and alter library information.
+ $info = libraries_info();
+ $this->assertTrue(isset($info['example_module']));
+ $this->verbose('Library:' . var_export($info['example_module'], TRUE) . '
');
+ $this->assertEqual($info['example_module']['info type'], 'module');
+ $this->assertEqual($info['example_module']['module'], 'libraries_test_module');
+ $this->assertTrue($info['example_module']['module_altered']);
+
+ // Test that themes can provide and alter library information.
+ $this->assertTrue(isset($info['example_theme']));
+ $this->verbose('Library:' . var_export($info['example_theme'], TRUE) . '
');
+ $this->assertEqual($info['example_theme']['info type'], 'theme');
+ $this->assertEqual($info['example_theme']['theme'], 'libraries_test_theme');
+ $this->assertTrue($info['example_theme']['theme_altered']);
+
// Test that library information is found correctly.
$expected = array(
'name' => 'Example files',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'files' => array(
'js' => array('example_1.js' => array()),
'css' => array('example_1.css' => array()),
'php' => array('example_1.php' => array()),
),
- 'module' => 'libraries_test',
+ 'info type' => 'module',
+ 'module' => 'libraries_test_module',
);
libraries_info_defaults($expected, 'example_files');
$library = libraries_info('example_files');
@@ -190,7 +209,8 @@ class LibrariesTestCase extends DrupalWebTestCase {
// Test a library specified with an .info file gets detected.
$expected = array(
'name' => 'Example info file',
- 'info file' => drupal_get_path('module', 'libraries_test') . '/example/example_info_file.libraries.info',
+ 'info type' => 'info file',
+ 'info file' => drupal_get_path('module', 'libraries') . '/tests/libraries/example_info_file.libraries.info',
);
libraries_info_defaults($expected, 'example_info_file');
$library = libraries_info('example_info_file');
@@ -303,6 +323,12 @@ class LibrariesTestCase extends DrupalWebTestCase {
$loaded = &drupal_static('libraries_load');
$this->verbose('' . var_export($loaded, TRUE) . '
');
$this->assertEqual($loaded['example_dependency']['loaded'], 1, 'Dependency library is also loaded');
+
+ // Test that PHP files that have a local $path variable do not break library
+ // loading.
+ // @see _libraries_require_once()
+ $library = libraries_load('example_path_variable_override');
+ $this->assertEqual($library['loaded'], 2, 'PHP files cannot break library loading.');
}
/**
@@ -311,7 +337,7 @@ class LibrariesTestCase extends DrupalWebTestCase {
function testCallbacks() {
$expected = array(
'name' => 'Example callback',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'versions' => array(
'1' => array(
@@ -344,12 +370,12 @@ class LibrariesTestCase extends DrupalWebTestCase {
),
),
'callbacks' => array(
- 'info' => array('_libraries_test_info_callback'),
- 'pre-detect' => array('_libraries_test_pre_detect_callback'),
- 'post-detect' => array('_libraries_test_post_detect_callback'),
- 'pre-dependencies-load' => array('_libraries_test_pre_dependencies_load_callback'),
- 'pre-load' => array('_libraries_test_pre_load_callback'),
- 'post-load' => array('_libraries_test_post_load_callback'),
+ 'info' => array('_libraries_test_module_info_callback'),
+ 'pre-detect' => array('_libraries_test_module_pre_detect_callback'),
+ 'post-detect' => array('_libraries_test_module_post_detect_callback'),
+ 'pre-dependencies-load' => array('_libraries_test_module_pre_dependencies_load_callback'),
+ 'pre-load' => array('_libraries_test_module_pre_load_callback'),
+ 'post-load' => array('_libraries_test_module_post_load_callback'),
),
'info callback' => 'not applied',
'pre-detect callback' => 'not applied',
@@ -357,7 +383,8 @@ class LibrariesTestCase extends DrupalWebTestCase {
'pre-dependencies-load callback' => 'not applied',
'pre-load callback' => 'not applied',
'post-load callback' => 'not applied',
- 'module' => 'libraries_test',
+ 'info type' => 'module',
+ 'module' => 'libraries_test_module',
);
libraries_info_defaults($expected, 'example_callback');
@@ -424,37 +451,46 @@ class LibrariesTestCase extends DrupalWebTestCase {
* We check for JavaScript and CSS files directly in the DOM and add a list of
* included PHP files manually to the page output.
*
- * @see _libraries_test_load()
+ * @see _libraries_test_module_load()
*/
function testLibrariesOutput() {
// Test loading of a simple library with a top-level files property.
- $this->drupalGet('libraries_test/files');
+ $this->drupalGet('libraries-test-module/files');
$this->assertLibraryFiles('example_1', 'File loading');
// Test loading of integration files.
- $this->drupalGet('libraries_test/integration_files');
- $this->assertRaw('libraries_test.js', 'Integration file loading: libraries_test.js found');
- $this->assertRaw('libraries_test.css', 'Integration file loading: libraries_test.css found');
- $this->assertRaw('libraries_test.inc', 'Integration file loading: libraries_test.inc found');
+ $this->drupalGet('libraries-test-module/module-integration-files');
+ $this->assertRaw('libraries_test_module.js', 'Integration file loading: libraries_test_module.js found');
+ $this->assertRaw('libraries_test_module.css', 'Integration file loading: libraries_test_module.css found');
+ $this->assertRaw('libraries_test_module.inc', 'Integration file loading: libraries_test_module.inc found');
+ $this->drupalGet('libraries-test-module/theme-integration-files');
+ $this->assertRaw('libraries_test_theme.js', 'Integration file loading: libraries_test_theme.js found');
+ $this->assertRaw('libraries_test_theme.css', 'Integration file loading: libraries_test_theme.css found');
+ $this->assertRaw('libraries_test_theme.inc', 'Integration file loading: libraries_test_theme.inc found');
+
+ // Test loading of post-load integration files.
+ $this->drupalGet('libraries-test-module/module-integration-files-post-load');
+ // If the files were not loaded correctly, a fatal error occurs.
+ $this->assertResponse(200, 'Post-load integration files are loaded correctly.');
// Test version overloading.
- $this->drupalGet('libraries_test/versions');
+ $this->drupalGet('libraries-test-module/versions');
$this->assertLibraryFiles('example_2', 'Version overloading');
// Test variant loading.
- $this->drupalGet('libraries_test/variant');
+ $this->drupalGet('libraries-test-module/variant');
$this->assertLibraryFiles('example_3', 'Variant loading');
// Test version overloading and variant loading.
- $this->drupalGet('libraries_test/versions_and_variants');
+ $this->drupalGet('libraries-test-module/versions-and-variants');
$this->assertLibraryFiles('example_4', 'Concurrent version and variant overloading');
// Test caching.
- variable_set('libraries_test_cache', TRUE);
+ variable_set('libraries_test_module_cache', TRUE);
cache_clear_all('example_callback', 'cache_libraries');
// When the library information is not cached, all callback groups should be
// invoked.
- $this->drupalGet('libraries_test/cache');
+ $this->drupalGet('libraries-test-module/cache');
$this->assertRaw('The info callback group was invoked.', 'Info callback invoked for uncached libraries.');
$this->assertRaw('The pre-detect callback group was invoked.', 'Pre-detect callback invoked for uncached libraries.');
$this->assertRaw('The post-detect callback group was invoked.', 'Post-detect callback invoked for uncached libraries.');
@@ -462,13 +498,13 @@ class LibrariesTestCase extends DrupalWebTestCase {
$this->assertRaw('The post-load callback group was invoked.', 'Post-load callback invoked for uncached libraries.');
// When the library information is cached only the 'pre-load' and
// 'post-load' callback groups should be invoked.
- $this->drupalGet('libraries_test/cache');
+ $this->drupalGet('libraries-test-module/cache');
$this->assertNoRaw('The info callback group was not invoked.', 'Info callback not invoked for cached libraries.');
$this->assertNoRaw('The pre-detect callback group was not invoked.', 'Pre-detect callback not invoked for cached libraries.');
$this->assertNoRaw('The post-detect callback group was not invoked.', 'Post-detect callback not invoked for cached libraries.');
$this->assertRaw('The pre-load callback group was invoked.', 'Pre-load callback invoked for cached libraries.');
$this->assertRaw('The post-load callback group was invoked.', 'Post-load callback invoked for cached libraries.');
- variable_set('libraries_test_cache', FALSE);
+ variable_set('libraries_test_module_cache', FALSE);
}
/**
@@ -516,7 +552,7 @@ class LibrariesTestCase extends DrupalWebTestCase {
foreach ($names as $name => $expected) {
foreach ($extensions as $extension) {
- $filepath = drupal_get_path('module', 'libraries_test') . "/example/$name.$extension";
+ $filepath = drupal_get_path('module', 'libraries') . "/tests/libraries/example/$name.$extension";
// JavaScript and CSS files appear as full URLs and with an appended
// query string.
if (in_array($extension, array('js', 'css'))) {
diff --git a/sites/all/modules/contrib/dev/libraries/tests/example/README.txt b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/README.txt
similarity index 76%
rename from sites/all/modules/contrib/dev/libraries/tests/example/README.txt
rename to sites/all/modules/contrib/dev/libraries/tests/libraries/example/README.txt
index e3582c2a..6c50a58a 100644
--- a/sites/all/modules/contrib/dev/libraries/tests/example/README.txt
+++ b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/README.txt
@@ -14,11 +14,11 @@ CSS and PHP files.
place the following text on the page:
"If this text shows up, no JavaScript test file was loaded."
This text is replaced via JavaScript by a text of the form:
- "If this text shows up, [[file] was loaded successfully."
+ "If this text shows up, [file] was loaded successfully."
[file] is either 'example_1.js', 'example_2.js', 'example_3.js',
- 'example_4.js' or 'libraries_test.js'. If you have SimpleTest's verbose mode
- enabled and see the above text in one of the debug pages, the noted JavaScript
- file was loaded successfully.
+ 'example_4.js' or 'libraries_test_module.js'. If you have SimpleTest's verbose
+ mode enabled and see the above text in one of the debug pages, the noted
+ JavaScript file was loaded successfully.
- CSS: The filenames of the CSS files are asserted to be in the raw HTML via
SimpleTest. Since the filename could appear, for instance, in an error
message, this is not very robust. Explicit testing of CSS, though, is not yet
@@ -29,7 +29,7 @@ CSS and PHP files.
- example_2: green
- example_3: orange
- example_4: blue
- - libraries_test: purple"
+ - libraries_test_module: purple"
If you have SimpleTest's verbose mode enabled, and see the above text in a
certain color (i.e. not in black), a CSS file was loaded successfully. Which
file depends on the color as referenced in the text above.
@@ -37,6 +37,7 @@ CSS and PHP files.
PHP files and then checking whether this function was defined using
function_exists(). This can be checked programatically with SimpleTest.
The loading of integration files is tested with the same method. The integration
-files are libraries_test.js, libraries_test.css, libraries_test.inc and are
-located in the tests directory alongside libraries_test.module (i.e. they are
-not in the same directory as this file).
+files are libraries_test_module.js, libraries_test_module.css,
+libraries_test_module.inc and are located in the test module's directory
+alongside libraries_test_module.info (i.e. they are not in the same directory as
+this file).
diff --git a/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.css b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.css
new file mode 100644
index 00000000..ed9ea3c2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.css
@@ -0,0 +1,12 @@
+
+/**
+ * @file
+ * Test CSS file for Libraries loading.
+ *
+ * Color the 'libraries-test-module-css' div red. See README.txt for more
+ * information.
+ */
+
+.libraries-test-module-css {
+ color: red;
+}
diff --git a/sites/all/modules/contrib/dev/libraries/tests/example/example_1.js b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.js
similarity index 52%
rename from sites/all/modules/contrib/dev/libraries/tests/example/example_1.js
rename to sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.js
index 8a1b9a27..659ff0ff 100644
--- a/sites/all/modules/contrib/dev/libraries/tests/example/example_1.js
+++ b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.js
@@ -3,7 +3,7 @@
* @file
* Test JavaScript file for Libraries loading.
*
- * Replace the text in the 'libraries-test-javascript' div. See README.txt for
+ * Replace the text in the 'libraries-test-module-js' div. See README.txt for
* more information.
*/
@@ -11,7 +11,7 @@
Drupal.behaviors.librariesTest = {
attach: function(context, settings) {
- $('.libraries-test-javascript').text('If this text shows up, example_1.js was loaded successfully.')
+ $('.libraries-test-module-js').text('If this text shows up, example_1.js was loaded successfully.')
}
};
diff --git a/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.php b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.php
new file mode 100644
index 00000000..4142e411
--- /dev/null
+++ b/sites/all/modules/contrib/dev/libraries/tests/libraries/example/example_1.php
@@ -0,0 +1,15 @@
+ 'Example module',
+ 'module_altered' => FALSE,
+ );
+
// Test library detection.
$libraries['example_missing'] = array(
'name' => 'Example missing',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/missing',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/missing',
);
$libraries['example_undetected_version'] = array(
'name' => 'Example undetected version',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests',
- 'version callback' => '_libraries_test_return_version',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
+ 'version callback' => '_libraries_test_module_return_version',
'version arguments' => array(FALSE),
);
$libraries['example_unsupported_version'] = array(
'name' => 'Example unsupported version',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests',
- 'version callback' => '_libraries_test_return_version',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
+ 'version callback' => '_libraries_test_module_return_version',
'version arguments' => array('1'),
'versions' => array(
'2' => array(),
),
);
-
$libraries['example_supported_version'] = array(
'name' => 'Example supported version',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests',
- 'version callback' => '_libraries_test_return_version',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
+ 'version callback' => '_libraries_test_module_return_version',
'version arguments' => array('1'),
'versions' => array(
'1' => array(),
@@ -43,7 +48,7 @@ function libraries_test_libraries_info() {
// Test the default version callback.
$libraries['example_default_version_callback'] = array(
'name' => 'Example default version callback',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version arguments' => array(
'file' => 'README.txt',
// Version 1
@@ -55,16 +60,16 @@ function libraries_test_libraries_info() {
// Test a multiple-parameter version callback.
$libraries['example_multiple_parameter_version_callback'] = array(
'name' => 'Example multiple parameter version callback',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
// Version 1
- 'version callback' => '_libraries_test_get_version',
+ 'version callback' => '_libraries_test_module_get_version',
'version arguments' => array('README.txt', '/Version (\d+)/', 5),
);
// Test a top-level files property.
$libraries['example_files'] = array(
'name' => 'Example files',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'files' => array(
'js' => array('example_1.js'),
@@ -76,23 +81,41 @@ function libraries_test_libraries_info() {
// Test loading of integration files.
// Normally added by the corresponding module via hook_libraries_info_alter(),
// these files should be automatically loaded when the library is loaded.
- $libraries['example_integration_files'] = array(
- 'name' => 'Example integration files',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ $libraries['example_module_integration_files'] = array(
+ 'name' => 'Example module integration files',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'integration files' => array(
- 'libraries_test' => array(
- 'js' => array('libraries_test.js'),
- 'css' => array('libraries_test.css'),
- 'php' => array('libraries_test.inc'),
+ 'libraries_test_module' => array(
+ 'js' => array('libraries_test_module.js'),
+ 'css' => array('libraries_test_module.css'),
+ 'php' => array('libraries_test_module.inc'),
),
),
);
+ // Test loading of integration files after library files.
+ // We test the correct loading order by calling a function that is defined in
+ // example_1.php in libraries_test_module_post_load.inc.
+ $libraries['example_module_integration_files_post_load'] = array(
+ 'name' => 'Example module post-load integration files',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
+ 'version' => '1',
+ 'files' => array(
+ 'php' => array('example_1.php'),
+ ),
+ 'integration files' => array(
+ 'libraries_test_module' => array(
+ 'php' => array('libraries_test_module_post_load.inc'),
+ ),
+ ),
+ 'post-load integration files' => TRUE,
+ );
+
// Test version overloading.
$libraries['example_versions'] = array(
'name' => 'Example versions',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '2',
'versions' => array(
'1' => array(
@@ -115,7 +138,7 @@ function libraries_test_libraries_info() {
// Test variant detection.
$libraries['example_variant_missing'] = array(
'name' => 'Example variant missing',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'variants' => array(
'example_variant' => array(
@@ -124,7 +147,7 @@ function libraries_test_libraries_info() {
'css' => array('example_3.css'),
'php' => array('example_3.php'),
),
- 'variant callback' => '_libraries_test_return_installed',
+ 'variant callback' => '_libraries_test_module_return_installed',
'variant arguments' => array(FALSE),
),
),
@@ -132,7 +155,7 @@ function libraries_test_libraries_info() {
$libraries['example_variant'] = array(
'name' => 'Example variant',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'variants' => array(
'example_variant' => array(
@@ -141,7 +164,7 @@ function libraries_test_libraries_info() {
'css' => array('example_3.css'),
'php' => array('example_3.php'),
),
- 'variant callback' => '_libraries_test_return_installed',
+ 'variant callback' => '_libraries_test_module_return_installed',
'variant arguments' => array(TRUE),
),
),
@@ -150,7 +173,7 @@ function libraries_test_libraries_info() {
// Test correct behaviour with multiple versions and multiple variants.
$libraries['example_versions_and_variants'] = array(
'name' => 'Example versions and variants',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '2',
'versions' => array(
'1' => array(
@@ -161,7 +184,7 @@ function libraries_test_libraries_info() {
'css' => array('example_1.css'),
'php' => array('example_1.php'),
),
- 'variant callback' => '_libraries_test_return_installed',
+ 'variant callback' => '_libraries_test_module_return_installed',
'variant arguments' => array(TRUE),
),
'example_variant_2' => array(
@@ -170,7 +193,7 @@ function libraries_test_libraries_info() {
'css' => array('example_2.css'),
'php' => array('example_2.php'),
),
- 'variant callback' => '_libraries_test_return_installed',
+ 'variant callback' => '_libraries_test_module_return_installed',
'variant arguments' => array(TRUE),
),
),
@@ -183,7 +206,7 @@ function libraries_test_libraries_info() {
'css' => array('example_3.css'),
'php' => array('example_3.php'),
),
- 'variant callback' => '_libraries_test_return_installed',
+ 'variant callback' => '_libraries_test_module_return_installed',
'variant arguments' => array(TRUE),
),
'example_variant_2' => array(
@@ -192,7 +215,7 @@ function libraries_test_libraries_info() {
'css' => array('example_4.css'),
'php' => array('example_4.php'),
),
- 'variant callback' => '_libraries_test_return_installed',
+ 'variant callback' => '_libraries_test_module_return_installed',
'variant arguments' => array(TRUE),
),
),
@@ -206,27 +229,27 @@ function libraries_test_libraries_info() {
// This library acts as a dependency for the libraries below.
$libraries['example_dependency'] = array(
'name' => 'Example dependency',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1.1',
'files' => array('js' => array('example_1.js')),
);
$libraries['example_dependency_missing'] = array(
'name' => 'Example dependency missing',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'dependencies' => array('example_missing'),
'files' => array('js' => array('example_1.js')),
);
$libraries['example_dependency_incompatible'] = array(
'name' => 'Example dependency incompatible',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'dependencies' => array('example_dependency (>1.1)'),
'files' => array('js' => array('example_1.js')),
);
$libraries['example_dependency_compatible'] = array(
'name' => 'Example dependency compatible',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'dependencies' => array('example_dependency (>=1.1)'),
'files' => array('js' => array('example_1.js')),
@@ -235,7 +258,7 @@ function libraries_test_libraries_info() {
// Test the applying of callbacks.
$libraries['example_callback'] = array(
'name' => 'Example callback',
- 'library path' => drupal_get_path('module', 'libraries') . '/tests/example',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
'version' => '1',
'versions' => array(
'1' => array(
@@ -271,12 +294,12 @@ function libraries_test_libraries_info() {
),
),
'callbacks' => array(
- 'info' => array('_libraries_test_info_callback'),
- 'pre-detect' => array('_libraries_test_pre_detect_callback'),
- 'post-detect' => array('_libraries_test_post_detect_callback'),
- 'pre-dependencies-load' => array('_libraries_test_pre_dependencies_load_callback'),
- 'pre-load' => array('_libraries_test_pre_load_callback'),
- 'post-load' => array('_libraries_test_post_load_callback'),
+ 'info' => array('_libraries_test_module_info_callback'),
+ 'pre-detect' => array('_libraries_test_module_pre_detect_callback'),
+ 'post-detect' => array('_libraries_test_module_post_detect_callback'),
+ 'pre-dependencies-load' => array('_libraries_test_module_pre_dependencies_load_callback'),
+ 'pre-load' => array('_libraries_test_module_pre_load_callback'),
+ 'post-load' => array('_libraries_test_module_post_load_callback'),
),
// These keys are for testing purposes only.
'info callback' => 'not applied',
@@ -287,24 +310,40 @@ function libraries_test_libraries_info() {
'post-load callback' => 'not applied',
);
+ $libraries['example_path_variable_override'] = array(
+ 'name' => 'Example path variable override',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
+ 'version' => '1',
+ 'files' => array(
+ 'php' => array('example_1.php', 'example_2.php'),
+ ),
+ );
+
return $libraries;
}
+/**
+ * Implements hook_libraries_info_alter().
+ */
+function libraries_test_module_libraries_info_alter(&$libraries) {
+ $libraries['example_module']['module_altered'] = TRUE;
+}
+
/**
* Implements hook_libraries_info_file_paths()
*/
-function libraries_test_libraries_info_file_paths() {
- return array(drupal_get_path('module', 'libraries_test') . '/example');
+function libraries_test_module_libraries_info_file_paths() {
+ return array(drupal_get_path('module', 'libraries') . '/tests/libraries');
}
/**
* Gets the version of an example library.
*
* Returns exactly the version string entered as the $version parameter. This
- * function cannot be collapsed with _libraries_test_return_installed(), because
- * of the different arguments that are passed automatically.
+ * function cannot be collapsed with _libraries_test_module_return_installed(),
+ * because of the different arguments that are passed automatically.
*/
-function _libraries_test_return_version($library, $version) {
+function _libraries_test_module_return_version($library, $version) {
return $version;
}
@@ -340,7 +379,7 @@ function _libraries_test_return_version($library, $version) {
*
* @see libraries_get_version()
*/
-function _libraries_test_get_version($library, $file, $pattern, $lines = 20, $cols = 200) {
+function _libraries_test_module_get_version($library, $file, $pattern, $lines = 20, $cols = 200) {
$file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $file;
if (!file_exists($file)) {
@@ -361,10 +400,10 @@ function _libraries_test_get_version($library, $file, $pattern, $lines = 20, $co
* Detects the variant of an example library.
*
* Returns exactly the value of $installed, either TRUE or FALSE. This function
- * cannot be collapsed with _libraries_test_return_version(), because of the
- * different arguments that are passed automatically.
+ * cannot be collapsed with _libraries_test_module_return_version(), because of
+ * the different arguments that are passed automatically.
*/
-function _libraries_test_return_installed($library, $name, $installed) {
+function _libraries_test_module_return_installed($library, $name, $installed) {
return $installed;
}
@@ -373,10 +412,10 @@ function _libraries_test_return_installed($library, $name, $installed) {
*
* This function is used as a test callback for the 'info' callback group.
*
- * @see _libraries_test_callback()
+ * @see _libraries_test_module_callback()
*/
-function _libraries_test_info_callback(&$library, $version, $variant) {
- _libraries_test_callback($library, $version, $variant, 'info');
+function _libraries_test_module_info_callback(&$library, $version, $variant) {
+ _libraries_test_module_callback($library, $version, $variant, 'info');
}
/**
@@ -384,10 +423,10 @@ function _libraries_test_info_callback(&$library, $version, $variant) {
*
* This function is used as a test callback for the 'pre-detect' callback group.
*
- * @see _libraries_test_callback()
+ * @see _libraries_test_module_callback()
*/
-function _libraries_test_pre_detect_callback(&$library, $version, $variant) {
- _libraries_test_callback($library, $version, $variant, 'pre-detect');
+function _libraries_test_module_pre_detect_callback(&$library, $version, $variant) {
+ _libraries_test_module_callback($library, $version, $variant, 'pre-detect');
}
/**
@@ -395,10 +434,10 @@ function _libraries_test_pre_detect_callback(&$library, $version, $variant) {
*
* This function is used as a test callback for the 'post-detect callback group.
*
- * @see _libraries_test_callback()
+ * @see _libraries_test_module_callback()
*/
-function _libraries_test_post_detect_callback(&$library, $version, $variant) {
- _libraries_test_callback($library, $version, $variant, 'post-detect');
+function _libraries_test_module_post_detect_callback(&$library, $version, $variant) {
+ _libraries_test_module_callback($library, $version, $variant, 'post-detect');
}
/**
@@ -407,10 +446,10 @@ function _libraries_test_post_detect_callback(&$library, $version, $variant) {
* This function is used as a test callback for the 'pre-dependencies-load'
* callback group.
*
- * @see _libraries_test_callback()
+ * @see _libraries_test_module_callback()
*/
-function _libraries_test_pre_dependencies_load_callback(&$library, $version, $variant) {
- _libraries_test_callback($library, $version, $variant, 'pre-dependencies-load');
+function _libraries_test_module_pre_dependencies_load_callback(&$library, $version, $variant) {
+ _libraries_test_module_callback($library, $version, $variant, 'pre-dependencies-load');
}
/**
@@ -418,10 +457,10 @@ function _libraries_test_pre_dependencies_load_callback(&$library, $version, $va
*
* This function is used as a test callback for the 'pre-load' callback group.
*
- * @see _libraries_test_callback()
+ * @see _libraries_test_module_callback()
*/
-function _libraries_test_pre_load_callback(&$library, $version, $variant) {
- _libraries_test_callback($library, $version, $variant, 'pre-load');
+function _libraries_test_module_pre_load_callback(&$library, $version, $variant) {
+ _libraries_test_module_callback($library, $version, $variant, 'pre-load');
}
/**
@@ -429,10 +468,10 @@ function _libraries_test_pre_load_callback(&$library, $version, $variant) {
*
* This function is used as a test callback for the 'post-load' callback group.
*
- * @see _libraries_test_callback()
+ * @see _libraries_test_module_callback()
*/
-function _libraries_test_post_load_callback(&$library, $version, $variant) {
- _libraries_test_callback($library, $version, $variant, 'post-load');
+function _libraries_test_module_post_load_callback(&$library, $version, $variant) {
+ _libraries_test_module_callback($library, $version, $variant, 'post-load');
}
/**
@@ -456,7 +495,7 @@ function _libraries_test_post_load_callback(&$library, $version, $variant) {
* The variant the library information passed in $library belongs to, or NULL
* if the passed library information is not variant-specific.
*/
-function _libraries_test_callback(&$library, $version, $variant, $group) {
+function _libraries_test_module_callback(&$library, $version, $variant, $group) {
$string = 'applied';
if (isset($version) && isset($variant)) {
$string .= " (version $version, variant $variant)";
@@ -475,7 +514,7 @@ function _libraries_test_callback(&$library, $version, $variant, $group) {
// The following is used to test caching of library information.
// Only set the message for the top-level library to prevent confusing,
// duplicate messages.
- if (!isset($version) && !isset($variant) && variable_get('libraries_test_cache', FALSE)) {
+ if (!isset($version) && !isset($variant) && variable_get('libraries_test_module_cache', FALSE)) {
drupal_set_message("The $group callback group was invoked.");
}
}
@@ -483,32 +522,40 @@ function _libraries_test_callback(&$library, $version, $variant, $group) {
/**
* Implements hook_menu().
*/
-function libraries_test_menu() {
+function libraries_test_module_menu() {
$base = array(
- 'page callback' => '_libraries_test_load',
+ 'page callback' => '_libraries_test_module_load',
'access callback' => TRUE,
);
- $items['libraries_test/files'] = $base + array(
+ $items['libraries-test-module/files'] = $base + array(
'title' => 'Test files',
'page arguments' => array('example_files'),
);
- $items['libraries_test/integration_files'] = $base + array(
- 'title' => 'Test integration files',
- 'page arguments' => array('example_integration_files'),
+ $items['libraries-test-module/module-integration-files'] = $base + array(
+ 'title' => 'Test module integration files',
+ 'page arguments' => array('example_module_integration_files'),
);
- $items['libraries_test/versions'] = $base + array(
+ $items['libraries-test-module/module-integration-files-post-load'] = $base + array(
+ 'title' => 'Test module post-load integration files',
+ 'page arguments' => array('example_module_integration_files_post_load'),
+ );
+ $items['libraries-test-module/theme-integration-files'] = $base + array(
+ 'title' => 'Test theme integration files',
+ 'page arguments' => array('example_theme_integration_files'),
+ );
+ $items['libraries-test-module/versions'] = $base + array(
'title' => 'Test version loading',
'page arguments' => array('example_versions'),
);
- $items['libraries_test/variant'] = $base + array(
+ $items['libraries-test-module/variant'] = $base + array(
'title' => 'Test variant loading',
'page arguments' => array('example_variant', 'example_variant'),
);
- $items['libraries_test/versions_and_variants'] = $base + array(
+ $items['libraries-test-module/versions-and-variants'] = $base + array(
'title' => 'Test concurrent version and variant loading',
'page arguments' => array('example_versions_and_variants', 'example_variant_2'),
);
- $items['libraries_test/cache'] = $base + array(
+ $items['libraries-test-module/cache'] = $base + array(
'title' => 'Test caching of library information',
'page arguments' => array('example_callback'),
);
@@ -523,7 +570,7 @@ function libraries_test_menu() {
* JavaScript and CSS files for easier debugging. See example/README.txt for
* more information.
*/
-function _libraries_test_load($library, $variant = NULL) {
+function _libraries_test_module_load($library, $variant = NULL) {
libraries_load($library, $variant);
// JavaScript and CSS files can be checked directly by SimpleTest, so we only
// need to manually check for PHP files.
@@ -532,14 +579,14 @@ function _libraries_test_load($library, $variant = NULL) {
// For easer debugging of JS loading, a text is shown that the JavaScript will
// replace.
$output .= 'JavaScript
';
- $output .= '';
+ $output .= '';
$output .= 'If this text shows up, no JavaScript test file was loaded.';
$output .= '';
// For easier debugging of CSS loading, the loaded CSS files will color the
// following text.
$output .= 'CSS
';
- $output .= '';
+ $output .= '';
$output .= 'If one of the CSS test files has been loaded, this text will be colored:';
$output .= '';
// Do not reference the actual CSS files (i.e. including '.css'), because that
@@ -548,17 +595,18 @@ function _libraries_test_load($library, $variant = NULL) {
$output .= '- example_2: green
';
$output .= '- example_3: orange
';
$output .= '- example_4: blue
';
- $output .= '- libraries_test: purple
';
+ $output .= '- libraries_test_module: purple
';
+ $output .= '- libraries_test_theme: turquoise
';
$output .= '
';
$output .= '';
$output .= 'PHP
';
- $output .= '';
+ $output .= '';
$output .= 'The following is a list of all loaded test PHP files:';
$output .= '';
$files = get_included_files();
foreach ($files as $file) {
- if (strpos($file, 'libraries/test') && !strpos($file, 'libraries_test.module')) {
+ if (strpos($file, 'libraries/test') && !strpos($file, 'libraries_test_module.module') && !strpos($file, 'template.php')) {
$output .= '- ' . str_replace(DRUPAL_ROOT . '/', '', $file) . '
';
}
}
@@ -567,3 +615,12 @@ function _libraries_test_load($library, $variant = NULL) {
return $output;
}
+
+/**
+ * Implements hook_system_theme_info().
+ */
+function libraries_test_module_system_theme_info() {
+ $themes = array();
+ $themes['libraries_test_theme'] = drupal_get_path('module', 'libraries') . '/tests/themes/libraries_test_theme/libraries_test_theme.info';
+ return $themes;
+}
diff --git a/sites/all/modules/contrib/dev/libraries/tests/modules/libraries_test_module/libraries_test_module_post_load.inc b/sites/all/modules/contrib/dev/libraries/tests/modules/libraries_test_module/libraries_test_module_post_load.inc
new file mode 100644
index 00000000..8e308f25
--- /dev/null
+++ b/sites/all/modules/contrib/dev/libraries/tests/modules/libraries_test_module/libraries_test_module_post_load.inc
@@ -0,0 +1,15 @@
+ 'Example theme',
+ 'theme_altered' => FALSE,
+ );
+ $libraries['example_theme_integration_files'] = array(
+ 'name' => 'Example theme integration file',
+ 'library path' => drupal_get_path('module', 'libraries') . '/tests/libraries/example',
+ 'version' => '1',
+ 'integration files' => array(
+ 'libraries_test_theme' => array(
+ 'js' => array('libraries_test_theme.js'),
+ 'css' => array('libraries_test_theme.css'),
+ 'php' => array('libraries_test_theme.inc'),
+ ),
+ ),
+ );
+ return $libraries;
+}
+
+/**
+ * Implements hook_libraries_info_alter().
+ */
+function libraries_test_theme_libraries_info_alter(&$libraries) {
+ $libraries['example_theme']['theme_altered'] = TRUE;
+}
diff --git a/sites/all/modules/contrib/dev/performance/README.txt b/sites/all/modules/contrib/dev/performance/README.txt
index 6fa0775b..4b43ae47 100644
--- a/sites/all/modules/contrib/dev/performance/README.txt
+++ b/sites/all/modules/contrib/dev/performance/README.txt
@@ -98,6 +98,33 @@ Statistics:
You can view the recorded performance statistics (summary and details) at
/admin/reports/performance-logging
+Custom detailed logging implementation
+--------------------------------------
+As mentioned before, detailed logging is NOT recommended on production environ-
+ments. If you, for whatever reason, DO wish detailed logging on production, you
+should create a custom detailed logging mechanism that will NOT interfere with
+your live site. You can do this by creating your own versions of the following
+functions:
+
+ - performance_log_details($params)
+ => function that is called to store the performance data
+ - performance_view_details()
+ => function that is called to view the stored detail log. This function is
+ called from hook_menu() and should return content that Drupal can render
+ as a page.
+ - performance_clear_details()
+ => function that is called to delete the entire detail log
+
+Have a look at includes/performance.details.inc for more details about these
+functions.
+
+When you have created those functions, add the location of the file containing
+your custom implementation to settings.php like so:
+
+ $conf['performance_detail_logging'] = './sites/all/path/to/your/file';
+
+NOTE: there is NO drush support for your custom detail logging implementation!
+
Drush support
-------------
Drush support has been integrated as well. You can check the summary and detail
diff --git a/sites/all/modules/contrib/dev/performance/includes/performance.details.inc b/sites/all/modules/contrib/dev/performance/includes/performance.details.inc
new file mode 100644
index 00000000..0c92ec2d
--- /dev/null
+++ b/sites/all/modules/contrib/dev/performance/includes/performance.details.inc
@@ -0,0 +1,137 @@
+ REQUEST_TIME,
+ 'bytes' => $params['mem'],
+ 'ms' => (int)$params['timer'],
+ 'query_count' => $params['query_count'],
+ 'query_timer' => (int)$params['query_timer'],
+ 'anon' => $params['anon'],
+ 'path' => $params['path'],
+ 'language' => $params['language'],
+ 'data' => $params['data'],
+ );
+
+ try {
+ db_insert('performance_detail')
+ ->fields($fields)
+ ->execute();
+ }
+ catch (Exception $e) {
+ watchdog_exception('performance', $e, NULL, array(), WATCHDOG_ERROR);
+ }
+}
+
+/**
+ * Detail page callback.
+ * @return array
+ * Drupal render array.
+ */
+function performance_view_details() {
+ drupal_set_title(t('Performance logs: Details'));
+
+ $header = array(
+ array('data' => t('#'), 'field' => 'pid', 'sort' => 'desc'),
+ array('data' => t('Path'), 'field' => 'path'),
+ array('data' => t('Date'), 'field' => 'timestamp'),
+ array('data' => t('Memory (MB)'), 'field' => 'bytes'),
+ array('data' => t('ms (Total)'), 'field' => 'ms'),
+ array('data' => t('Language'), 'field' => 'language'),
+ array('data' => t('Anonymous?'), 'field' => 'anon'),
+ );
+ if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
+ $header[] = array('data' => t('# Queries'), 'field' => 'query_count');
+ $header[] = array('data' => t('Query ms'), 'field' => 'query_timer');
+ }
+
+ $pager_height = 50;
+ $result = db_select('performance_detail', 'p')
+ ->fields('p')
+ ->extend('PagerDefault')
+ ->limit($pager_height)
+ ->extend('TableSort')
+ ->orderByHeader($header)
+ ->execute();
+
+ $rows = array();
+
+ foreach ($result as $data) {
+ $row_data = array();
+ $row_data[] = $data->pid;
+ $row_data[] = l($data->path, $data->path);
+ $row_data[] = format_date($data->timestamp, 'small');
+ $row_data[] = number_format($data->bytes / 1024 / 1024, 2);
+ $row_data[] = $data->ms;
+ $row_data[] = $data->language;
+ $row_data[] = ($data->anon) ? t('Yes') : t('No');
+
+ if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
+ $row_data[] = $data->query_count;
+ $row_data[] = $data->query_timer;
+ }
+
+ $rows[] = array('data' => $row_data);
+ }
+
+ if (empty($rows) && !variable_get('performance_detail', 0)) {
+ return array(
+ 'content' => array(
+ '#markup' => t('Detail performance log is not enabled. Go to the !link to enable it.', array('!link' => l(t('settings page'), PERFORMANCE_SETTINGS, array('query' => drupal_get_destination()))))
+ ),
+ );
+ }
+ elseif (!variable_get('performance_detail', 0)) {
+ drupal_set_message(t('Detail performance log is not enabled! Showing stored logs.'), 'warning');
+ }
+
+ // Return a renderable array.
+ return array(
+ 'query_data_detail' => array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#sticky' => TRUE,
+ '#empty' => t('No log messages available.'),
+ ),
+ 'clear' => array(
+ '#markup' => l(t('Clear logs'), 'admin/reports/performance-logging/clear/details'),
+ ),
+ 'pager' => array(
+ '#theme' => 'pager',
+ '#quantity' => $pager_height,
+ ),
+ );
+}
+
+/**
+ * Helper function to clear the detail logs.
+ * @return void
+ */
+function performance_clear_details() {
+ db_truncate('performance_detail')->execute();
+}
+
+/**
+ * Helper function to prune detail log on cron.
+ * @return void
+ */
+function performance_prune_details() {
+ db_delete('performance_detail')
+ ->condition('timestamp', REQUEST_TIME - (24 * 60 * 60), '<=')
+ ->execute();
+}
diff --git a/sites/all/modules/contrib/dev/performance/performance.info b/sites/all/modules/contrib/dev/performance/performance.info
index 51908ae0..5c481efd 100644
--- a/sites/all/modules/contrib/dev/performance/performance.info
+++ b/sites/all/modules/contrib/dev/performance/performance.info
@@ -6,9 +6,9 @@ configure = admin/config/development/performance-logging
tags[] = developer
tags[] = monitoring
-; Information added by drupal.org packaging script on 2013-01-12
-version = "7.x-2.0-beta1"
+; Information added by packaging script on 2013-11-09
+version = "7.x-2.0"
core = "7.x"
project = "performance"
-datestamp = "1358019516"
+datestamp = "1384025307"
diff --git a/sites/all/modules/contrib/dev/performance/performance.module b/sites/all/modules/contrib/dev/performance/performance.module
index eee5c1bb..7129aa8d 100644
--- a/sites/all/modules/contrib/dev/performance/performance.module
+++ b/sites/all/modules/contrib/dev/performance/performance.module
@@ -23,6 +23,8 @@ define('PERFORMANCE_CACHE', 'cache_default_class');
define('PERFORMANCE_SETTINGS', 'admin/config/development/performance-logging');
+include_once variable_get('performance_detail_logging', 'includes/performance.details.inc');
+
/**
* Implements hook_menu().
*/
@@ -101,9 +103,9 @@ function performance_cron() {
performance_traverse_cache('performance_cron_prune');
// Remove performance_detail rows on a daily basis.
- db_delete('performance_detail')
- ->condition('timestamp', REQUEST_TIME - (24 * 60 * 60), '<=')
- ->execute();
+ if (variable_get('performance_detail', 0)) {
+ performance_prune_details();
+ }
}
/**
@@ -345,7 +347,7 @@ function performance_log_summary($params) {
}
// Keep records for 1 day.
- $expire = $result['last_access'] + (24 * 60 * 60);
+ $expire = $result['data']['last_access'] + (24 * 60 * 60);
cache_set($key, $result['data'], PERFORMANCE_BIN, $expire);
}
@@ -481,32 +483,6 @@ function performance_get_summary($cache, $timestamp) {
return;
}
-/**
- * Helper function to store detailed data in database.
- */
-function performance_log_details($params = array()) {
- $fields = array(
- 'timestamp' => REQUEST_TIME,
- 'bytes' => $params['mem'],
- 'ms' => (int)$params['timer'],
- 'query_count' => $params['query_count'],
- 'query_timer' => (int)$params['query_timer'],
- 'anon' => $params['anon'],
- 'path' => $params['path'],
- 'language' => $params['language'],
- 'data' => $params['data'],
- );
-
- try {
- db_insert('performance_detail')
- ->fields($fields)
- ->execute();
- }
- catch (Exception $e) {
- watchdog_exception('performance', $e, NULL, array(), WATCHDOG_ERROR);
- }
-}
-
/**
* Summary page callback.
*/
@@ -688,85 +664,6 @@ function performance_sort_summary($data, $direction, $field) {
return $data;
}
-/**
- * Detail page callback.
- */
-function performance_view_details() {
- drupal_set_title(t('Performance logs: Details'));
-
- $header = array(
- array('data' => t('#'), 'field' => 'pid', 'sort' => 'desc'),
- array('data' => t('Path'), 'field' => 'path'),
- array('data' => t('Date'), 'field' => 'timestamp'),
- array('data' => t('Memory (MB)'), 'field' => 'bytes'),
- array('data' => t('ms (Total)'), 'field' => 'ms'),
- array('data' => t('Language'), 'field' => 'language'),
- array('data' => t('Anonymous?'), 'field' => 'anon'),
- );
- if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
- $header[] = array('data' => t('# Queries'), 'field' => 'query_count');
- $header[] = array('data' => t('Query ms'), 'field' => 'query_timer');
- }
-
- $pager_height = 50;
- $result = db_select('performance_detail', 'p')
- ->fields('p')
- ->extend('PagerDefault')
- ->limit($pager_height)
- ->extend('TableSort')
- ->orderByHeader($header)
- ->execute();
-
- $rows = array();
-
- foreach ($result as $data) {
- $row_data = array();
- $row_data[] = $data->pid;
- $row_data[] = l($data->path, $data->path);
- $row_data[] = format_date($data->timestamp, 'small');
- $row_data[] = number_format($data->bytes / 1024 / 1024, 2);
- $row_data[] = $data->ms;
- $row_data[] = $data->language;
- $row_data[] = ($data->anon) ? t('Yes') : t('No');
-
- if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
- $row_data[] = $data->query_count;
- $row_data[] = $data->query_timer;
- }
-
- $rows[] = array('data' => $row_data);
- }
-
- if (empty($rows) && !variable_get('performance_detail', 0)) {
- return array(
- 'content' => array(
- '#markup' => t('Detail performance log is not enabled. Go to the !link to enable it.', array('!link' => l(t('settings page'), PERFORMANCE_SETTINGS, array('query' => drupal_get_destination()))))
- ),
- );
- }
- elseif (!variable_get('performance_detail', 0)) {
- drupal_set_message(t('Detail performance log is not enabled! Showing stored logs.'), 'warning');
- }
-
- // Return a renderable array.
- return array(
- 'query_data_detail' => array(
- '#theme' => 'table',
- '#header' => $header,
- '#rows' => $rows,
- '#sticky' => TRUE,
- '#empty' => t('No log messages available.'),
- ),
- 'clear' => array(
- '#markup' => l(t('Clear logs'), 'admin/reports/performance-logging/clear/details'),
- ),
- 'pager' => array(
- '#theme' => 'pager',
- '#quantity' => $pager_height,
- ),
- );
-}
-
/**
* Clear logs form.
*/
@@ -812,7 +709,7 @@ function performance_clear_form_submit($form, &$form_state) {
cache_clear_all('*', PERFORMANCE_BIN, TRUE);
break;
case 'details':
- db_truncate('performance_detail')->execute();
+ performance_clear_details();
break;
}
diff --git a/sites/all/modules/contrib/editor/pathologic/pathologic.api.php b/sites/all/modules/contrib/editor/pathologic/pathologic.api.php
index fce2e63a..09760f9f 100644
--- a/sites/all/modules/contrib/editor/pathologic/pathologic.api.php
+++ b/sites/all/modules/contrib/editor/pathologic/pathologic.api.php
@@ -72,7 +72,7 @@ function hook_pathologic_alter(&$url_params, $parts, $settings) {
}
// If it's a path to a local image, make sure it's using our CDN server.
- if (preg_match('~\.(png|gif|jpe?g)$', $url_params['path'])) {
+ if (preg_match('~\.(png|gif|jpe?g)$~', $url_params['path'])) {
$url_params['path'] = 'http://cdn.example.com/' . $url_params['path'];
$url_params['options']['external'] = TRUE;
}
diff --git a/sites/all/modules/contrib/editor/pathologic/pathologic.info b/sites/all/modules/contrib/editor/pathologic/pathologic.info
index c4950943..0016e52c 100644
--- a/sites/all/modules/contrib/editor/pathologic/pathologic.info
+++ b/sites/all/modules/contrib/editor/pathologic/pathologic.info
@@ -5,9 +5,9 @@ dependencies[] = filter
core = 7.x
files[] = pathologic.test
-; Information added by drupal.org packaging script on 2013-07-09
-version = "7.x-2.11"
+; Information added by Drupal.org packaging script on 2013-12-14
+version = "7.x-2.12"
core = "7.x"
project = "pathologic"
-datestamp = "1373385363"
+datestamp = "1387055607"
diff --git a/sites/all/modules/contrib/editor/pathologic/pathologic.module b/sites/all/modules/contrib/editor/pathologic/pathologic.module
index 52a8ec95..806fd3f1 100644
--- a/sites/all/modules/contrib/editor/pathologic/pathologic.module
+++ b/sites/all/modules/contrib/editor/pathologic/pathologic.module
@@ -75,6 +75,16 @@ function _pathologic_settings($form, &$form_state, $filter, $format, $defaults,
* if language path prefixing (eg /ja/node/123) is in use. REMEMBER THIS IN THE
* FUTURE, ALBRIGHT.
*
+ * The below code uses the @ operator before parse_url() calls because in PHP
+ * 5.3.2 and earlier, parse_url() causes a warning of parsing fails. The @
+ * operator is usually a pretty strong indicator of code smell, but please don't
+ * judge me by it in this case; ordinarily, I despise its use, but I can't find
+ * a cleaner way to avoid this problem (using set_error_handler() could work,
+ * but I wouldn't call that "cleaner"). Fortunately, Drupal 8 will require at
+ * least PHP 5.3.5, so this mess doesn't have to spread into the D8 branch of
+ * Pathologic.
+ * @see https://drupal.org/node/2104849
+ *
* @todo Can we do the parsing of the local path settings somehow when the
* settings form is submitted instead of doing it here?
*/
@@ -82,14 +92,14 @@ function _pathologic_filter($text, $filter, $format, $langcode, $cache, $cache_i
// Get the base URL and explode it into component parts. We add these parts
// to the exploded local paths settings later.
global $base_url;
- $base_url_parts = parse_url($base_url . '/');
+ $base_url_parts = @parse_url($base_url . '/');
// Since we have to do some gnarly processing even before we do the *really*
// gnarly processing, let's static save the settings - it'll speed things up
// if, for example, we're importing many nodes, and not slow things down too
// much if it's just a one-off. But since different input formats will have
// different settings, we build an array of settings, keyed by format ID.
- $settings = &drupal_static(__FUNCTION__, array());
- if (!isset($settings[$filter->format])) {
+ $cached_settings = &drupal_static(__FUNCTION__, array());
+ if (!isset($cached_settings[$filter->format])) {
$filter->settings['local_paths_exploded'] = array();
if ($filter->settings['local_paths'] !== '') {
// Build an array of the exploded local paths for this format's settings.
@@ -98,7 +108,7 @@ function _pathologic_filter($text, $filter, $format, $langcode, $cache, $cache_i
// @see http://drupal.org/node/1727492
$local_paths = array_filter(array_map('trim', explode("\n", $filter->settings['local_paths'])));
foreach ($local_paths as $local) {
- $parts = parse_url($local);
+ $parts = @parse_url($local);
// Okay, what the hellish "if" statement is doing below is checking to
// make sure we aren't about to add a path to our array of exploded
// local paths which matches the current "local" path. We consider it
@@ -145,37 +155,39 @@ function _pathologic_filter($text, $filter, $format, $langcode, $cache, $cache_i
// We'll also just store the host part separately for easy access.
$filter->settings['base_url_host'] = $base_url_parts['host'];
- $settings[$filter->format] = $filter->settings;
+ $cached_settings[$filter->format] = $filter->settings;
}
// Get the language code for the text we're about to process.
- $settings['langcode'] = $langcode;
+ $cached_settings['langcode'] = $langcode;
// And also take note of which settings in the settings array should apply.
- $settings['current_settings'] = &$settings[$filter->format];
+ $cached_settings['current_settings'] = &$cached_settings[$filter->format];
// Now that we have all of our settings prepared, attempt to process all
// paths in href, src, action or longdesc HTML attributes. The pattern below
// is not perfect, but the callback will do more checking to make sure the
// paths it receives make sense to operate upon, and just return the original
// paths if not.
- return preg_replace_callback('~(href|src|action|longdesc)="([^"]+)~i', '_pathologic_replace', $text);
+ return preg_replace_callback('~ (href|src|action|longdesc)="([^"]+)~i', '_pathologic_replace', $text);
}
/**
* Process and replace paths. preg_replace_callback() callback.
*/
function _pathologic_replace($matches) {
+ // Get the base path.
+ global $base_path;
+
// Get the settings for the filter. Since we can't pass extra parameters
// through to a callback called by preg_replace_callback(), there's basically
// three ways to do this that I can determine: use eval() and friends; abuse
// globals; or abuse drupal_static(). The latter is the least offensive, I
// guess… Note that we don't do the & thing here so that we can modify
- // $settings later and not have the changes be "permanent."
- $settings = drupal_static('_pathologic_filter');
+ // $cached_settings later and not have the changes be "permanent."
+ $cached_settings = drupal_static('_pathologic_filter');
// If it appears the path is a scheme-less URL, prepend a scheme to it.
// parse_url() cannot properly parse scheme-less URLs. Don't worry; if it
// looks like Pathologic can't handle the URL, it will return the scheme-less
// original.
-
// @see https://drupal.org/node/1617944
// @see https://drupal.org/node/2030789
if (strpos($matches[2], '//') === 0) {
@@ -190,7 +202,7 @@ function _pathologic_replace($matches) {
// @see http://drupal.org/node/1672932
$original_url = htmlspecialchars_decode($matches[2]);
// …and parse the URL
- $parts = parse_url($original_url);
+ $parts = @parse_url($original_url);
// Do some more early tests to see if we should just give up now.
if (
// If parse_url() failed, give up.
@@ -227,7 +239,7 @@ function _pathologic_replace($matches) {
if (isset($parts['scheme']) && $parts['scheme'] === 'files') {
// Path Filter "files:" support. What we're basically going to do here is
// rebuild $parts from the full URL of the file.
- $new_parts = parse_url(file_create_url(file_default_scheme() . '://' . $parts['path']));
+ $new_parts = @parse_url(file_create_url(file_default_scheme() . '://' . $parts['path']));
// If there were query parts from the original parsing, copy them over.
if (!empty($parts['query'])) {
$new_parts['query'] = $parts['query'];
@@ -235,10 +247,10 @@ function _pathologic_replace($matches) {
$new_parts['path'] = rawurldecode($new_parts['path']);
$parts = $new_parts;
// Don't do language handling for file paths.
- $settings['is_file'] = TRUE;
+ $cached_settings['is_file'] = TRUE;
}
else {
- $settings['is_file'] = FALSE;
+ $cached_settings['is_file'] = FALSE;
}
// Let's also bail out of this doesn't look like a local path.
@@ -246,7 +258,7 @@ function _pathologic_replace($matches) {
// Cycle through local paths and find one with a host and a path that matches;
// or just a host if that's all we have; or just a starting path if that's
// what we have.
- foreach ($settings['current_settings']['local_paths_exploded'] as $exploded) {
+ foreach ($cached_settings['current_settings']['local_paths_exploded'] as $exploded) {
// If a path is available in both…
if (isset($exploded['path']) && isset($parts['path'])
// And the paths match…
@@ -283,7 +295,7 @@ function _pathologic_replace($matches) {
// e.g.: if our url is /foo/bar we'll mark this as a match for
// http://example.com but want to keep searching and would prefer a match
// to http://example.com/foo if that's configured as a local path
- elseif (!isset($parts['host']) && (!isset($exploded['path']) || $exploded['path'] == '/')) {
+ elseif (!isset($parts['host']) && (!isset($exploded['path']) || $exploded['path'] === $base_path)) {
$found = TRUE;
}
}
@@ -326,12 +338,12 @@ function _pathologic_replace($matches) {
// If we didn't previously identify this as a file, check to see if the file
// exists now that we have the correct path relative to DRUPAL_ROOT
- if (!$settings['is_file']){
- $settings['is_file'] = !empty($parts['path']) && is_file(DRUPAL_ROOT . '/'. $parts['path']);
+ if (!$cached_settings['is_file']) {
+ $cached_settings['is_file'] = !empty($parts['path']) && is_file(DRUPAL_ROOT . '/'. $parts['path']);
}
// Okay, deal with language stuff.
- if ($settings['is_file']) {
+ if ($cached_settings['is_file']) {
// If we're linking to a file, use a fake LANGUAGE_NONE language object.
// Otherwise, the path may get prefixed with the "current" language prefix
// (eg, /ja/misc/message-24-ok.png)
@@ -365,7 +377,7 @@ function _pathologic_replace($matches) {
'fragment' => isset($parts['fragment']) ? $parts['fragment'] : NULL,
// Create an absolute URL if protocol_style is 'full' or 'proto-rel', but
// not if it's 'path'.
- 'absolute' => $settings['current_settings']['protocol_style'] !== 'path',
+ 'absolute' => $cached_settings['current_settings']['protocol_style'] !== 'path',
// If we seem to have found a language for the path, pass it along to
// url(). Otherwise, ignore the 'language' parameter.
'language' => isset($parts['language_obj']) ? $parts['language_obj'] : NULL,
@@ -381,7 +393,7 @@ function _pathologic_replace($matches) {
// Now alter!
// @see http://drupal.org/node/1762022
- drupal_alter('pathologic', $url_params, $parts, $settings);
+ drupal_alter('pathologic', $url_params, $parts, $cached_settings);
// If any of the alter hooks asked us to just pass along the original URL,
// then do so.
@@ -396,9 +408,9 @@ function _pathologic_replace($matches) {
// @see http://drupal.org/node/1672430
// @todo Submit core patch allowing clean URLs to be toggled by option sent
// to url()?
- if (!empty($settings['is_file'])) {
- $settings['orig_clean_url'] = !empty($GLOBALS['conf']['clean_url']);
- if (!$settings['orig_clean_url']) {
+ if (!empty($cached_settings['is_file'])) {
+ $cached_settings['orig_clean_url'] = !empty($GLOBALS['conf']['clean_url']);
+ if (!$cached_settings['orig_clean_url']) {
$GLOBALS['conf']['clean_url'] = TRUE;
}
}
@@ -408,20 +420,20 @@ function _pathologic_replace($matches) {
// If we turned clean URLs on before to create a path to a file, turn them
// back off.
- if ($settings['is_file'] && !$settings['orig_clean_url']) {
+ if ($cached_settings['is_file'] && !$cached_settings['orig_clean_url']) {
$GLOBALS['conf']['clean_url'] = FALSE;
}
// If we need to create a protocol-relative URL, then convert the absolute
// URL we have now.
- if ($settings['current_settings']['protocol_style'] === 'proto-rel') {
+ if ($cached_settings['current_settings']['protocol_style'] === 'proto-rel') {
// Now, what might have happened here is that url() returned a URL which
// isn't on "this" server due to a hook_url_outbound_alter() implementation.
// We don't want to convert the URL in that case. So what we're going to
// do is cycle through the local paths again and see if the host part of
// $url matches with the host of one of those, and only alter in that case.
- $url_parts = parse_url($url);
- if (!empty($url_parts['host']) && $url_parts['host'] === $settings['current_settings']['base_url_host']) {
+ $url_parts = @parse_url($url);
+ if (!empty($url_parts['host']) && $url_parts['host'] === $cached_settings['current_settings']['base_url_host']) {
$url = _pathologic_url_to_protocol_relative($url);
}
}
@@ -430,7 +442,7 @@ function _pathologic_replace($matches) {
// @see http://drupal.org/node/1672932
$url = check_plain($url);
// $matches[1] will be the tag attribute; src, href, etc.
- return "{$matches[1]}=\"{$url}";
+ return " {$matches[1]}=\"{$url}";
}
/**
diff --git a/sites/all/modules/contrib/fields/email/email.diff.inc b/sites/all/modules/contrib/fields/email/email.diff.inc
new file mode 100644
index 00000000..b9a54ff8
--- /dev/null
+++ b/sites/all/modules/contrib/fields/email/email.diff.inc
@@ -0,0 +1,17 @@
+ $item) {
+ $diff_items[$delta] = $item['email'];
+ }
+ return $diff_items;
+}
diff --git a/sites/all/modules/contrib/fields/email/email.info b/sites/all/modules/contrib/fields/email/email.info
index 3bfb2dca..a4dd1d2b 100644
--- a/sites/all/modules/contrib/fields/email/email.info
+++ b/sites/all/modules/contrib/fields/email/email.info
@@ -5,9 +5,9 @@ package = Fields
files[] = email.migrate.inc
-; Information added by drupal.org packaging script on 2012-08-29
-version = "7.x-1.2"
+; Information added by Drupal.org packaging script on 2014-04-10
+version = "7.x-1.3"
core = "7.x"
project = "email"
-datestamp = "1346254131"
+datestamp = "1397134155"
diff --git a/sites/all/modules/contrib/fields/email/email.migrate.inc b/sites/all/modules/contrib/fields/email/email.migrate.inc
index 1b6272a2..b068acbe 100644
--- a/sites/all/modules/contrib/fields/email/email.migrate.inc
+++ b/sites/all/modules/contrib/fields/email/email.migrate.inc
@@ -10,7 +10,7 @@ class MigrateEmailFieldHandler extends MigrateFieldHandler {
$this->registerTypes(array('email'));
}
- public function prepare(stdClass $entity, array $field_info, array $instance, array $values) {
+ public function prepare($entity, array $field_info, array $instance, array $values) {
// Setup the Field API array for saving.
$arguments = (isset($values['arguments'])) ? $values['arguments']: array();
$language = $this->getFieldLanguage($entity, $field_info, $arguments);
diff --git a/sites/all/modules/contrib/fields/email/email.module b/sites/all/modules/contrib/fields/email/email.module
index dd92fdf7..a33d85c3 100644
--- a/sites/all/modules/contrib/fields/email/email.module
+++ b/sites/all/modules/contrib/fields/email/email.module
@@ -1,5 +1,9 @@
2);
+ return array(
+ 'api' => 2,
+ 'field handlers' => array('MigrateEmailFieldHandler'),
+ );
}
/**
@@ -198,7 +205,7 @@ function email_menu() {
'page callback' => 'drupal_get_form',
'page arguments' => array('email_admin_settings'),
'access arguments' => array('administer site configuration'),
- 'type' => MENU_CALLBACK,
+ 'type' => MENU_NORMAL_ITEM,
);
return $items;
}
@@ -433,7 +440,7 @@ function email_mail_page_form_submit($form, &$form_state) {
// Log the operation:
flood_register_event('email');
- watchdog('mail', t('%name-from sent an e-mail at %form.', array('%name-from' => $form_state['values']['name'], '%form' => url($_GET['q'], array('absolute' => TRUE)))));
+ watchdog('mail', '%name-from sent an e-mail at %form.', array('%name-from' => $form_state['values']['name'], '%form' => url($_GET['q'], array('absolute' => TRUE))));
drupal_set_message(t('Your message has been sent.'));
$form_state['redirect'] = $path;
@@ -447,7 +454,7 @@ function email_mail($key, &$message, $params) {
switch ($key) {
case 'contact':
// Compose the body:
- $message['body'][] = t('@name sent a message using the contact form at @url.', array('@name' => $params['name'], '@url' => $params['url']), array('langcode' =>$language->language));
+ $message['body'][] = t('@name sent a message using the contact form at @url.', array('@name' => $params['name'], '@url' => $params['url']), array('langcode' => $language->language));
$message['body'][] = $params['message'];
$message['subject'] = "";
diff --git a/sites/all/modules/contrib/fields/link/link-rtl.css b/sites/all/modules/contrib/fields/link/link-rtl.css
new file mode 100644
index 00000000..0359487e
--- /dev/null
+++ b/sites/all/modules/contrib/fields/link/link-rtl.css
@@ -0,0 +1,8 @@
+.link-field-column {
+ float: right;
+}
+
+.link-field-column.link-field-url .form-text {
+ direction: ltr;
+ text-align: left;
+}
diff --git a/sites/all/modules/contrib/fields/link/link.devel_generate.inc b/sites/all/modules/contrib/fields/link/link.devel_generate.inc
index 7be4a0dd..af0e2d5d 100644
--- a/sites/all/modules/contrib/fields/link/link.devel_generate.inc
+++ b/sites/all/modules/contrib/fields/link/link.devel_generate.inc
@@ -21,9 +21,12 @@ function link_devel_generate($object, $field, $instance, $bundle) {
* Callback for hook_devel_generate().
*/
function _link_devel_generate($object, $field, $instance, $bundle) {
- return array(
+ $link = array(
'url' => url('', array('absolute' => TRUE)),
- 'title' => devel_create_greeking(mt_rand(1, 3), TRUE),
'attributes' => _link_default_attributes(),
);
+ if ($instance['settings']['title'] != 'none') {
+ $link['title'] = devel_create_greeking(mt_rand(1, 3), TRUE);
+ }
+ return $link;
}
diff --git a/sites/all/modules/contrib/fields/link/link.info b/sites/all/modules/contrib/fields/link/link.info
index 57aedb39..016b7990 100644
--- a/sites/all/modules/contrib/fields/link/link.info
+++ b/sites/all/modules/contrib/fields/link/link.info
@@ -18,9 +18,9 @@ files[] = tests/link.validate.test
files[] = views/link_views_handler_argument_target.inc
files[] = views/link_views_handler_filter_protocol.inc
-; Information added by drupal.org packaging script on 2013-02-09
-version = "7.x-1.1"
+; Information added by Drupal.org packaging script on 2014-10-21
+version = "7.x-1.3"
core = "7.x"
project = "link"
-datestamp = "1360444361"
+datestamp = "1413924830"
diff --git a/sites/all/modules/contrib/fields/link/link.migrate.inc b/sites/all/modules/contrib/fields/link/link.migrate.inc
index 6388d11e..fc8363f6 100644
--- a/sites/all/modules/contrib/fields/link/link.migrate.inc
+++ b/sites/all/modules/contrib/fields/link/link.migrate.inc
@@ -97,7 +97,12 @@ class MigrateLinkFieldHandler extends MigrateFieldHandler {
}
}
if (isset($arguments['attributes'])) {
- $item['attributes'] = $arguments['attributes'];
+ if (is_array($arguments['attributes']) && isset($arguments['attributes'][$delta])) {
+ $item['attributes'] = $arguments['attributes'][$delta];
+ }
+ else {
+ $item['attributes'] = $arguments['attributes'];
+ }
}
$item['url'] = $value;
$return[$language][$delta] = $item;
diff --git a/sites/all/modules/contrib/fields/link/link.module b/sites/all/modules/contrib/fields/link/link.module
index c42e9466..b0c53c37 100644
--- a/sites/all/modules/contrib/fields/link/link.module
+++ b/sites/all/modules/contrib/fields/link/link.module
@@ -10,7 +10,7 @@ define('LINK_INTERNAL', 'internal');
define('LINK_FRONT', 'front');
define('LINK_EMAIL', 'email');
define('LINK_NEWS', 'news');
-define('LINK_DOMAINS', 'aero|arpa|asia|biz|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|mobi|local');
+define('LINK_DOMAINS', 'aero|arpa|asia|biz|build|com|cat|ceo|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|post|pro|tel|travel|mobi|local|xxx');
define('LINK_TARGET_DEFAULT', 'default');
define('LINK_TARGET_NEW_WINDOW', '_blank');
@@ -46,12 +46,14 @@ function link_field_info() {
'url' => 0,
'title' => 'optional',
'title_value' => '',
+ 'title_label_use_field_label' => FALSE,
'title_maxlength' => 128,
'enable_tokens' => 1,
'display' => array(
'url_cutoff' => 80,
),
'validate_url' => 1,
+ 'absolute_url' => 1,
),
'default_widget' => 'link_field',
'default_formatter' => 'link_default',
@@ -70,6 +72,13 @@ function link_field_instance_settings_form($field, $instance) {
'#element_validate' => array('link_field_settings_form_validate'),
);
+ $form['absolute_url'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Absolute URL'),
+ '#default_value' => isset($instance['settings']['absolute_url']) && ($instance['settings']['absolute_url'] !== '') ? $instance['settings']['absolute_url'] : TRUE,
+ '#description' => t('If checked, the URL will always render as an absolute URL.'),
+ );
+
$form['validate_url'] = array(
'#type' => 'checkbox',
'#title' => t('Validate URL'),
@@ -107,6 +116,13 @@ function link_field_instance_settings_form($field, $instance) {
'#description' => t('This title will always be used if “Static Title” is selected above.'),
);
+ $form['title_label_use_field_label'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Use field label as the label for the title field'),
+ '#default_value' => isset($instance['settings']['title_label_use_field_label']) ? $instance['settings']['title_label_use_field_label'] : FALSE,
+ '#description' => t('If this is checked the field label will be hidden.'),
+ );
+
$form['title_maxlength'] = array(
'#type' => 'textfield',
'#title' => t('Max length of title field'),
@@ -117,28 +133,22 @@ function link_field_instance_settings_form($field, $instance) {
);
if (module_exists('token')) {
- // Add token module replacements fields
- $form['tokens'] = array(
- '#type' => 'fieldset',
- '#collapsible' => TRUE,
- '#collapsed' => TRUE,
- '#title' => t('Placeholder tokens'),
- '#description' => t("The following placeholder tokens can be used in both paths and titles. When used in a path or title, they will be replaced with the appropriate values."),
- );
- $entity_info = entity_get_info($instance['entity_type']);
- $form['tokens']['help'] = array(
- '#theme' => 'token_tree',
- '#token_types' => array($entity_info['token type']),
- '#global_types' => TRUE,
- '#click_insert' => TRUE,
- );
-
+ // Add token module replacements fields.
$form['enable_tokens'] = array(
'#type' => 'checkbox',
'#title' => t('Allow user-entered tokens'),
'#default_value' => isset($instance['settings']['enable_tokens']) ? $instance['settings']['enable_tokens'] : 1,
'#description' => t('Checking will allow users to enter tokens in URLs and Titles on the entity edit form. This does not affect the field settings on this page.'),
);
+
+ $entity_info = entity_get_info($instance['entity_type']);
+ $form['tokens_help'] = array(
+ '#theme' => 'token_tree',
+ '#token_types' => array($entity_info['token type']),
+ '#global_types' => TRUE,
+ '#click_insert' => TRUE,
+ '#dialog' => TRUE,
+ );
}
$form['display'] = array(
@@ -189,6 +199,11 @@ function link_field_instance_settings_form($field, $instance) {
'#description' => t('Turn on/off if rel attribute should be removed automatically, if user given link is internal/external'),
'#options' => $rel_remove_options,
);
+ $form['attributes']['configurable_class'] = array(
+ '#title' => t("Allow the user to enter a custom link class per link"),
+ '#type' => 'checkbox',
+ '#default_value' => empty($instance['settings']['attributes']['configurable_class']) ? '' : $instance['settings']['attributes']['configurable_class'],
+ );
$form['attributes']['class'] = array(
'#type' => 'textfield',
'#title' => t('Additional CSS Class'),
@@ -258,12 +273,16 @@ function link_field_validate($entity_type, $entity, $field, $instance, $langcode
$optional_field_found = FALSE;
if ($instance['settings']['validate_url'] !== 0 || is_null($instance['settings']['validate_url']) || !isset($instance['settings']['validate_url'])) {
foreach ($items as $delta => $value) {
- _link_validate($items[$delta], $delta, $field, $entity, $instance, $langcode, $optional_field_found);
+ _link_validate($items[$delta], $delta, $field, $entity, $instance, $langcode, $optional_field_found, $errors);
}
}
if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] && !$optional_field_found) {
- form_set_error($field['field_name'] . '][' . $langcode . '][0][title', t('At least one title or URL must be entered.'));
+ $errors[$field['field_name']][$langcode][0][] = array(
+ 'error' => 'link_required',
+ 'message' => t('At least one title or URL must be entered.'),
+ 'error_element' => array('url' => FALSE, 'title' => TRUE),
+ );
}
}
@@ -320,6 +339,18 @@ function link_field_widget_form(&$form, &$form_state, $field, $instance, $langco
return $element;
}
+/**
+ * Implements hook_field_widget_error().
+ */
+function link_field_widget_error($element, $error, $form, &$form_state) {
+ if ($error['error_element']['title']) {
+ form_error($element['title'], $error['message']);
+ }
+ elseif ($error['error_element']['url']) {
+ form_error($element['url'], $error['message']);
+ }
+}
+
/**
* Unpacks the item attributes for use.
*/
@@ -341,9 +372,11 @@ function _link_load($field, $item, $instance) {
/**
* Prepares the item attributes and url for storage.
*/
-function _link_process(&$item, $delta = 0, $field, $entity) {
+function _link_process(&$item, $delta, $field, $entity) {
// Trim whitespace from URL.
- $item['url'] = trim($item['url']);
+ if (!empty($item['url'])) {
+ $item['url'] = trim($item['url']);
+ }
// If no attributes are set then make sure $item['attributes'] is an empty
// array, so $field['attributes'] can override it.
@@ -367,41 +400,61 @@ function _link_process(&$item, $delta = 0, $field, $entity) {
/**
* Validates that the link field has been entered properly.
*/
-function _link_validate(&$item, $delta, $field, $entity, $instance, $langcode, &$optional_field_found) {
+function _link_validate(&$item, $delta, $field, $entity, $instance, $langcode, &$optional_field_found, &$errors) {
if ($item['url'] && !(isset($instance['default_value'][$delta]['url']) && $item['url'] === $instance['default_value'][$delta]['url'] && !$instance['required'])) {
// Validate the link.
if (link_validate_url(trim($item['url'])) == FALSE) {
- form_set_error($field['field_name'] . '][' . $langcode . '][' . $delta . '][url', t('The value provided for %field is not a valid URL.', array('%field' => $instance['label'])));
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'link_required',
+ 'message' => t('The value %value provided for %field is not a valid URL.', array(
+ '%value' => trim($item['url']),
+ '%field' => $instance['label'],
+ )),
+ 'error_element' => array('url' => TRUE, 'title' => FALSE),
+ );
}
// Require a title for the link if necessary.
if ($instance['settings']['title'] == 'required' && strlen(trim($item['title'])) == 0) {
- form_set_error($field['field_name'] . '][' . $langcode . '][' . $delta . '][title', t('Titles are required for all links.'));
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'link_required',
+ 'message' => t('Titles are required for all links.'),
+ 'error_element' => array('url' => FALSE, 'title' => TRUE),
+ );
}
}
// Require a link if we have a title.
if ($instance['settings']['url'] !== 'optional' && strlen(isset($item['title']) ? $item['title'] : NULL) > 0 && strlen(trim($item['url'])) == 0) {
- form_set_error($field['field_name'] . '][' . $langcode . '][' . $delta . '][url', t('You cannot enter a title without a link url.'));
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'link_required',
+ 'message' => t('You cannot enter a title without a link url.'),
+ 'error_element' => array('url' => TRUE, 'title' => FALSE),
+ );
}
// In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link.
- if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
+ if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional'
+ && (strlen(trim($item['url'])) !== 0 || strlen(trim($item['title'])) !== 0)) {
$optional_field_found = TRUE;
}
- // Require entire field
+ // Require entire field.
if ($instance['settings']['url'] === 'optional' && $instance['settings']['title'] === 'optional' && $instance['required'] == 1 && !$optional_field_found && isset($instance['id'])) {
- form_set_error($instance['field_name'] . '][' . $langcode . '][0][title', t('At least one title or URL must be entered.'));
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'link_required',
+ 'message' => t('At least one title or URL must be entered.'),
+ 'error_element' => array('url' => FALSE, 'title' => TRUE),
+ );
}
}
/**
* Clean up user-entered values for a link field according to field settings.
*
- * @param $item
+ * @param array $item
* A single link item, usually containing url, title, and attributes.
- * @param $delta
+ * @param int $delta
* The delta value if this field is one of multiple fields.
- * @param $field
+ * @param array $field
* The CCK field definition.
- * @param $entity
+ * @param object $entity
* The entity containing this link.
*/
function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
@@ -409,6 +462,9 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
if (empty($item['url']) && empty($item['title'])) {
return;
}
+ if (empty($item['html'])) {
+ $item['html'] = FALSE;
+ }
// Replace URL tokens.
$entity_type = $instance['entity_type'];
@@ -439,19 +495,13 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
$url = link_cleanup_url($item['url']);
$url_parts = _link_parse_url($url);
- // We can't check_plain('') because it'll break.
- if ($type != LINK_FRONT) {
- $url_parts['url'] = check_plain($url_parts['url']);
- }
-
if (!empty($url_parts['url'])) {
- $item['url'] = url($url_parts['url'],
- array(
- 'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
- 'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
- 'absolute' => TRUE,
- 'html' => TRUE,
- )
+ $item['url'] = $url_parts['url'];
+ $item += array(
+ 'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
+ 'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
+ 'absolute' => !empty($instance['settings']['absolute_url']),
+ 'html' => TRUE,
);
}
@@ -464,7 +514,7 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
array(
'query' => isset($url_parts['query']) ? $url_parts['query'] : NULL,
'fragment' => isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
- 'absolute' => TRUE,
+ 'absolute' => !empty($instance['settings']['absolute_url']),
)
);
}
@@ -489,7 +539,7 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
$title = '';
}
- // Replace tokens.
+ // Replace title tokens.
if ($title && ($instance['settings']['title'] == 'value' || $instance['settings']['enable_tokens'])) {
// Load the entity if necessary for entities in views.
if (isset($entity->{$property_id})) {
@@ -511,7 +561,7 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
// Unserialize attributtes array if it has not been unserialized yet.
if (!is_array($item['attributes'])) {
- $item['attributes'] = (array)unserialize($item['attributes']);
+ $item['attributes'] = (array) unserialize($item['attributes']);
}
// Add default attributes.
@@ -560,6 +610,16 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
$item['attributes']['title'] = token_replace($item['attributes']['title'], array($entity_token_type => $entity_loaded));
$item['attributes']['title'] = filter_xss($item['attributes']['title'], array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u'));
}
+ // Handle attribute classes.
+ if (!empty($item['attributes']['class'])) {
+ $classes = explode(' ', $item['attributes']['class']);
+ foreach ($classes as &$class) {
+ $class = drupal_html_class($class);
+ }
+ $item['attributes']['class'] = implode(' ', $classes);
+ }
+ unset($item['attributes']['configurable_class']);
+
// Remove title attribute if it's equal to link text.
if (isset($item['attributes']['title']) && $item['attributes']['title'] == $item['title']) {
unset($item['attributes']['title']);
@@ -568,15 +628,6 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
// Remove empty attributes.
$item['attributes'] = array_filter($item['attributes']);
-
- // Sets title to trimmed url if one exists
- // @todo: Obsolete?
- /*if(!empty($item['display_url']) && empty($item['title'])) {
- $item['title'] = $item['display_url'];
- }
- elseif(!isset($item['title'])) {
- $item['title'] = $item['url'];
- }*/
}
/**
@@ -585,7 +636,7 @@ function _link_sanitize(&$item, $delta, &$field, $instance, &$entity) {
* @param string $url
* URL to parse.
*
- * @return Array
+ * @return array
* Array of url pieces - only 'url', 'query', and 'fragment'.
*/
function _link_parse_url($url) {
@@ -598,51 +649,77 @@ function _link_parse_url($url) {
// Separate out the query string, if any.
if (strpos($url, '?') !== FALSE) {
$query = substr($url, strpos($url, '?') + 1);
- parse_str($query, $query_array);
- // See http://drupal.org/node/1710578
- foreach ($query_array as $key=> &$value) {
- if ($value === '' && FALSE === strpos($query, $key . '=')) {
- $value = NULL;
- }
- }
- $url_parts['query'] = $query_array;
+ $url_parts['query'] = _link_parse_str($query);
$url = substr($url, 0, strpos($url, '?'));
}
$url_parts['url'] = $url;
return $url_parts;
}
+/**
+ * Replaces the PHP parse_str() function.
+ *
+ * Because parse_str replaces the following characters in query parameters name
+ * in order to maintain compability with deprecated register_globals directive:
+ *
+ * - chr(32) ( ) (space)
+ * - chr(46) (.) (dot)
+ * - chr(91) ([) (open square bracket)
+ * - chr(128) - chr(159) (various)
+ *
+ * @param string $query
+ * Query to parse.
+ *
+ * @return array
+ * Array of query parameters.
+ *
+ * @see http://php.net/manual/en/language.variables.external.php#81080
+ */
+function _link_parse_str($query) {
+ $query_array = array();
+
+ $pairs = explode('&', $query);
+ foreach ($pairs as $pair) {
+ $name_value = explode('=', $pair, 2);
+ $name = urldecode($name_value[0]);
+ $value = isset($name_value[1]) ? urldecode($name_value[1]) : NULL;
+ $query_array[$name] = $value;
+ }
+
+ return $query_array;
+}
+
/**
* Implements hook_theme().
*/
function link_theme() {
return array(
'link_formatter_link_default' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_plain' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_absolute' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_domain' => array(
- 'variables' => array('element' => NULL, 'display' => NULL),
+ 'variables' => array('element' => NULL, 'display' => NULL, 'field' => NULL),
),
'link_formatter_link_title_plain' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_url' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_short' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_label' => array(
'variables' => array('element' => NULL, 'field' => NULL),
),
'link_formatter_link_separate' => array(
- 'variables' => array('element' => NULL),
+ 'variables' => array('element' => NULL, 'field' => NULL),
),
'link_field' => array(
'render element' => 'element',
@@ -668,7 +745,7 @@ function theme_link_field($vars) {
if (isset($element['title'])) {
$output .= '' . drupal_render($element['title']) . '';
}
- $output .= ''. drupal_render($element['url']) . '';
+ $output .= '' . drupal_render($element['url']) . '';
$output .= '
';
if (!empty($element['attributes']['target'])) {
$output .= '' . drupal_render($element['attributes']['target']) . '';
@@ -676,6 +753,10 @@ function theme_link_field($vars) {
if (!empty($element['attributes']['title'])) {
$output .= '' . drupal_render($element['attributes']['title']) . '';
}
+ if (!empty($element['attributes']['class'])) {
+ $output .= '' . drupal_render($element['attributes']['class']) . '';
+ }
+ $output .= drupal_render_children($element);
return $output;
}
@@ -723,10 +804,21 @@ function link_field_process($element, $form_state, $complete_form) {
'#default_value' => isset($element['#value']['url']) ? $element['#value']['url'] : NULL,
);
if ($settings['title'] !== 'none' && $settings['title'] !== 'value') {
+ // Figure out the label of the title field.
+ if (!empty($settings['title_label_use_field_label'])) {
+ // Use the element label as the title field label.
+ $title_label = $element['#title'];
+ // Hide the field label because there is no need for the duplicate labels.
+ $element['#title_display'] = 'invisible';
+ }
+ else {
+ $title_label = t('Title');
+ }
+
$element['title'] = array(
'#type' => 'textfield',
'#maxlength' => $settings['title_maxlength'],
- '#title' => t('Title'),
+ '#title' => $title_label,
'#description' => t('The link title is limited to @maxlength characters maximum.', array('@maxlength' => $settings['title_maxlength'])),
'#required' => ($settings['title'] == 'required' && (($element['#delta'] == 0 && $element['#required']) || !empty($element['#value']['url']))) ? TRUE : FALSE,
'#default_value' => isset($element['#value']['title']) ? $element['#value']['title'] : NULL,
@@ -757,6 +849,15 @@ function link_field_process($element, $form_state, $complete_form) {
'#field_suffix' => '"',
);
}
+ if (!empty($settings['attributes']['configurable_class']) && $settings['attributes']['configurable_class'] == 1) {
+ $element['attributes']['class'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Custom link class'),
+ '#default_value' => isset($attributes['class']) ? $attributes['class'] : '',
+ '#field_prefix' => 'class = "',
+ '#field_suffix' => '"',
+ );
+ }
// If the title field is avaliable or there are field accepts multiple values
// then allow the individual field items display the required asterisk if needed.
@@ -886,14 +987,13 @@ function theme_link_formatter_link_default($vars) {
if (isset($link_options['attributes']['class'])) {
$link_options['attributes']['class'] = array($link_options['attributes']['class']);
}
-
// Display a normal link if both title and URL are available.
if (!empty($vars['element']['title']) && !empty($vars['element']['url'])) {
return l($vars['element']['title'], $vars['element']['url'], $link_options);
}
// If only a title, display the title.
elseif (!empty($vars['element']['title'])) {
- return check_plain($vars['element']['title']);
+ return $link_options['html'] ? $vars['element']['title'] : check_plain($vars['element']['title']);
}
elseif (!empty($vars['element']['url'])) {
return l($vars['element']['title'], $vars['element']['url'], $link_options);
@@ -916,7 +1016,7 @@ function theme_link_formatter_link_plain($vars) {
}
/**
- * Formats a link as an absolute URL
+ * Formats a link as an absolute URL.
*/
function theme_link_formatter_link_absolute($vars) {
$absolute = array('absolute' => TRUE);
@@ -985,10 +1085,8 @@ function theme_link_formatter_link_separate($vars) {
unset($link_options['url']);
$title = empty($vars['element']['title']) ? '' : check_plain($vars['element']['title']);
- /**
- * @TODO static html markup looks not very elegant
- * needs smarter output solution and an optional title/url seperator
- */
+ // @TODO static html markup looks not very elegant
+ // needs smarter output solution and an optional title/url seperator
$url_parts = _link_parse_url($vars['element']['url']);
$output = '';
$output .= '';
@@ -1002,6 +1100,8 @@ function theme_link_formatter_link_separate($vars) {
/**
* Implements hook_token_list().
+ *
+ * @TODO: hook_token_list no longer exists - this should change to hook_token_info().
*/
function link_token_list($type = 'all') {
if ($type === 'field' || $type === 'all') {
@@ -1013,6 +1113,11 @@ function link_token_list($type = 'all') {
}
}
+/**
+ * Implements hook_token_values().
+ *
+ * @TODO: hook_token_values no longer exists - this should change to hook_tokens().
+ */
function link_token_values($type, $object = NULL) {
if ($type === 'field') {
$item = $object[0];
@@ -1042,6 +1147,7 @@ function link_views_api() {
* protocol specified
*
* @param string $url
+ * The url entered by the user.
* @param string $protocol
* The protocol to be prepended to the url if one is not specified
*/
@@ -1072,6 +1178,7 @@ function link_cleanup_url($url, $protocol = 'http') {
* addresses following the RFC 2368 standard for mailto address formation.
*
* @param string $text
+ * Url to be validated.
*
* @return mixed
* Returns boolean FALSE if the URL is not valid. On success, returns one of
@@ -1141,7 +1248,7 @@ function link_validate_url($text) {
$allowed_protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'));
$LINK_DOMAINS = _link_domains();
- // Starting a parenthesis group with (?: means that it is grouped, but is not captured
+ // Starting a parenthesis group with (?: means that it is grouped, but is not captured.
$protocol = '((?:' . implode("|", $allowed_protocols) . '):\/\/)';
$authentication = "(?:(?:(?:[\w\.\-\+!$&'\(\)*\+,;=" . $LINK_ICHARS . "]|%[0-9a-f]{2})+(?::(?:[\w" . $LINK_ICHARS . "\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})*)?)?@)";
$domain = '(?:(?:[a-z0-9' . $LINK_ICHARS_DOMAIN . ']([a-z0-9' . $LINK_ICHARS_DOMAIN . '\-_\[\]])*)(\.(([a-z0-9' . $LINK_ICHARS_DOMAIN . '\-_\[\]])+\.)*(' . $LINK_DOMAINS . '|[a-z]{2}))?)';
@@ -1158,7 +1265,7 @@ function link_validate_url($text) {
$directories = "(?:\/[a-z0-9" . $LINK_ICHARS . "_\-\.~+%=&,$'#!():;*@\[\]]*)*";
// Yes, four backslashes == a single backslash.
- $query = "(?:\/?\?([?a-z0-9" . $LINK_ICHARS . "+_|\-\.~\/\\\\%=&,$'():;*@\[\]{} ]*))";
+ $query = "(?:\/?\?([?a-z0-9" . $LINK_ICHARS . "+_|\-\.~\/\\\\%=&,$'!():;*@\[\]{} ]*))";
$anchor = "(?:#[a-z0-9" . $LINK_ICHARS . "_\-\.~+%=&,$'():;*@\[\]\/\?]*)";
// The rest of the path for a standard URL.
@@ -1169,7 +1276,7 @@ function link_validate_url($text) {
$news_pattern = '/^news:(' . $newsgroup_name . '|' . $message_id . ')$/i';
$user = '[a-zA-Z0-9' . $LINK_ICHARS . '_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'\[\]]+';
- $email_pattern = '/^mailto:' . $user . '@'.'(?:' . $domain . '|' . $ipv4 . '|' . $ipv6 . '|localhost)' . $query . '?$/';
+ $email_pattern = '/^mailto:' . $user . '@' . '(?:' . $domain . '|' . $ipv4 . '|' . $ipv6 . '|localhost)' . $query . '?$/';
if (strpos($text, '') === 0) {
return LINK_FRONT;
@@ -1251,7 +1358,7 @@ function link_field_settings_form() {
/**
* Additional callback to adapt the property info of link fields.
*
- * @see entity_metadata_field_entity_property_info().
+ * @see entity_metadata_field_entity_property_info()
*/
function link_field_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
$property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
@@ -1295,16 +1402,27 @@ function link_field_item_property_info() {
'label' => t('The URL of the link.'),
'setter callback' => 'entity_property_verbatim_set',
);
+ $properties['attributes'] = array(
+ 'type' => 'struct',
+ 'label' => t('The attributes of the link.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'getter callback' => 'link_attribute_property_get',
+ );
return $properties;
}
+/**
+ * Entity property info getter callback for link attributes.
+ */
+function link_attribute_property_get($data, array $options, $name, $type, $info) {
+ return isset($data[$name]) ? array_filter($data[$name]) : array();
+}
+
/**
* Implements hook_field_update_instance().
*/
function link_field_update_instance($instance, $prior_instance) {
- if (function_exists('i18n_string_update')
- && isset($prior_instance['settings']['title_value']) && isset($instance['settings']['title_value'])
- && $prior_instance['settings']['title_value'] != $instance['settings']['title_value']) {
+ if (function_exists('i18n_string_update') && $instance['widget']['type'] == 'link_field' && $prior_instance['settings']['title_value'] != $instance['settings']['title_value']) {
$i18n_string_name = "field:{$instance['field_name']}:{$instance['bundle']}:title_value";
i18n_string_update($i18n_string_name, $instance['settings']['title_value']);
}
@@ -1314,9 +1432,10 @@ function link_field_update_instance($instance, $prior_instance) {
* Implements hook_i18n_string_list_TEXTGROUP_alter().
*/
function link_i18n_string_list_field_alter(&$strings, $type = NULL, $object = NULL) {
- if ($type == 'field_instance' && $object && $object['widget']['type'] == 'link_field') {
- if (isset($object['settings']['title_value'])) {
- $strings['field'][$object['field_name']][$object['bundle']]['title_value']['string'] = $object['settings']['title_value'];
- }
+ if ($type != 'field_instance' || !is_array($object) || !isset($object['widget']['type'])) {
+ return;
+ }
+ if ($object['widget']['type'] == 'link_field' && isset($object['settings']['title_value'])) {
+ $strings['field'][$object['field_name']][$object['bundle']]['title_value']['string'] = $object['settings']['title_value'];
}
}
diff --git a/sites/all/modules/contrib/fields/link/tests/link.attribute.test b/sites/all/modules/contrib/fields/link/tests/link.attribute.test
index 97ac2a47..603847ef 100644
--- a/sites/all/modules/contrib/fields/link/tests/link.attribute.test
+++ b/sites/all/modules/contrib/fields/link/tests/link.attribute.test
@@ -46,8 +46,8 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
protected function assertLinkOnNode($field_name, $link_value, $message = '', $group = 'Other') {
$this->zebra++;
$zebra_string = ($this->zebra % 2 == 0) ? 'even' : 'odd';
- $cssFieldLocator = 'field-'. str_replace('_', '-', $field_name);
- $this->assertPattern('@\s*'. $link_value .'\s*@is',
+ $cssFieldLocator = 'field-' . str_replace('_', '-', $field_name);
+ $this->assertPattern('@\s*' . $link_value . '\s*@is',
$message,
$group);
}
@@ -66,7 +66,7 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
// Create the content type.
$this->clickLink(t('Add content type'));
- $edit = array (
+ $edit = array(
'name' => $content_type_friendly,
'type' => $content_type_machine,
);
@@ -76,8 +76,8 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
// Now add a singleton field.
$single_field_name_friendly = $this->randomName(20);
$single_field_name_machine = strtolower($this->randomName(10));
- $single_field_name = 'field_'. $single_field_name_machine;
- $edit = array (
+ $single_field_name = 'field_' . $single_field_name_machine;
+ $edit = array(
'fields[_add_new_field][label]' => $single_field_name_friendly,
'fields[_add_new_field][field_name]' => $single_field_name_machine,
'fields[_add_new_field][type]' => 'link_field',
@@ -112,19 +112,19 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->drupalLogin($this->web_user);
// Go to page.
- $this->drupalGet('node/add/'. $content_type_machine);
+ $this->drupalGet('node/add/' . $content_type_machine);
// Add a node.
$edit = array(
'title' => $title,
- 'field_'. $single_field_name_machine. '[und][0][title]' => 'Link',
- 'field_'. $single_field_name_machine. '[und][0][url]' => 'http://www.drupal.org/',
+ 'field_' . $single_field_name_machine . '[und][0][title]' => 'Link',
+ 'field_' . $single_field_name_machine . '[und][0][url]' => 'http://www.drupal.org/',
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText(t('@content_type_friendly @title has been created', array('@content_type_friendly' => $content_type_friendly, '@title' => $title)));
- $this->drupalGet('node/add/'. $content_type_machine);
+ $this->drupalGet('node/add/' . $content_type_machine);
// Create a node:
$edit = array(
@@ -143,7 +143,7 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
protected function createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine) {
$this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/fields');
- $edit = array (
+ $edit = array(
'fields[_add_new_field][label]' => $single_field_name_friendly,
'fields[_add_new_field][field_name]' => $single_field_name_machine,
'fields[_add_new_field][type]' => 'link_field',
@@ -180,7 +180,7 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
}
protected function createNodeForTesting($content_type_machine, $content_type_friendly, $single_field_name_machine, $title, $url, $node_title = '') {
- $this->drupalGet('node/add/'. $content_type_machine);
+ $this->drupalGet('node/add/' . $content_type_machine);
if (!$node_title) {
$node_title = $this->randomName(20);
@@ -219,10 +219,10 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine);
// Okay, now we want to make sure this display is changed:
- $this->drupalGet('admin/structure/types/manage/'. $content_type_machine .'/display');
+ $this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/display');
$edit = array(
- 'fields[field_'. $single_field_name_machine .'][label]' => 'above',
- 'fields[field_'. $single_field_name_machine .'][type]' => 'link_plain',
+ 'fields[field_' . $single_field_name_machine . '][label]' => 'above',
+ 'fields[field_' . $single_field_name_machine . '][type]' => 'link_plain',
);
$this->drupalPost(NULL, $edit, t('Save'));
@@ -270,10 +270,10 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine);
// Okay, now we want to make sure this display is changed:
- $this->drupalGet('admin/structure/types/manage/'. $content_type_machine .'/display');
+ $this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/display');
$edit = array(
- 'fields[field_'. $single_field_name_machine .'][label]' => 'above',
- 'fields[field_'. $single_field_name_machine .'][type]' => 'link_url',
+ 'fields[field_' . $single_field_name_machine . '][label]' => 'above',
+ 'fields[field_' . $single_field_name_machine . '][type]' => 'link_url',
);
$this->drupalPost(NULL, $edit, t('Save'));
@@ -320,10 +320,10 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine);
// Okay, now we want to make sure this display is changed:
- $this->drupalGet('admin/structure/types/manage/'. $content_type_machine .'/display');
+ $this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/display');
$edit = array(
- 'fields[field_'. $single_field_name_machine .'][label]' => 'above',
- 'fields[field_'. $single_field_name_machine .'][type]' => 'link_short',
+ 'fields[field_' . $single_field_name_machine . '][label]' => 'above',
+ 'fields[field_' . $single_field_name_machine . '][type]' => 'link_short',
);
$this->drupalPost(NULL, $edit, t('Save'));
@@ -371,10 +371,10 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine);
// Okay, now we want to make sure this display is changed:
- $this->drupalGet('admin/structure/types/manage/'. $content_type_machine .'/display');
+ $this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/display');
$edit = array(
- 'fields[field_'. $single_field_name_machine .'][label]' => 'above',
- 'fields[field_'. $single_field_name_machine .'][type]' => 'link_label',
+ 'fields[field_' . $single_field_name_machine . '][label]' => 'above',
+ 'fields[field_' . $single_field_name_machine . '][type]' => 'link_label',
);
$this->drupalPost(NULL, $edit, t('Save'));
@@ -422,10 +422,10 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine);
// Okay, now we want to make sure this display is changed:
- $this->drupalGet('admin/structure/types/manage/'. $content_type_machine .'/display');
+ $this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/display');
$edit = array(
- 'fields[field_'. $single_field_name_machine .'][label]' => 'above',
- 'fields[field_'. $single_field_name_machine .'][type]' => 'link_separate',
+ 'fields[field_' . $single_field_name_machine . '][label]' => 'above',
+ 'fields[field_' . $single_field_name_machine . '][type]' => 'link_separate',
);
$this->drupalPost(NULL, $edit, t('Save'));
@@ -474,10 +474,10 @@ class LinkAttributeCrudTest extends DrupalWebTestCase {
$this->createSimpleLinkField($single_field_name_machine, $single_field_name_friendly, $content_type_machine);
// Okay, now we want to make sure this display is changed:
- $this->drupalGet('admin/structure/types/manage/'. $content_type_machine .'/display');
+ $this->drupalGet('admin/structure/types/manage/' . $content_type_machine . '/display');
$edit = array(
- 'fields[field_'. $single_field_name_machine .'][label]' => 'above',
- 'fields[field_'. $single_field_name_machine .'][type]' => 'link_title_plain',
+ 'fields[field_' . $single_field_name_machine . '][label]' => 'above',
+ 'fields[field_' . $single_field_name_machine . '][type]' => 'link_title_plain',
);
$this->drupalPost(NULL, $edit, t('Save'));
diff --git a/sites/all/modules/contrib/fields/link/tests/link.crud.test b/sites/all/modules/contrib/fields/link/tests/link.crud.test
index 54889fd2..e9b7db41 100644
--- a/sites/all/modules/contrib/fields/link/tests/link.crud.test
+++ b/sites/all/modules/contrib/fields/link/tests/link.crud.test
@@ -37,18 +37,17 @@ class LinkContentCrudTest extends DrupalWebTestCase {
// Create the content type.
$this->clickLink(t('Add content type'));
- $edit = array (
+ $edit = array(
'name' => $content_type_friendly,
'type' => $content_type_machine,
);
$this->drupalPost(NULL, $edit, t('Save and add fields'));
$this->assertText(t('The content type @name has been added.', array('@name' => $content_type_friendly)));
- //$field = $this->createField(array('type' => 'link', 'widget_type' => 'link'), 0);
// Now add a singleton field.
$single_field_name_friendly = $this->randomName(20);
$single_field_name_machine = strtolower($this->randomName(10));
- $edit = array (
+ $edit = array(
'fields[_add_new_field][label]' => $single_field_name_friendly,
'fields[_add_new_field][field_name]' => $single_field_name_machine,
'fields[_add_new_field][type]' => 'link_field',
@@ -69,14 +68,5 @@ class LinkContentCrudTest extends DrupalWebTestCase {
menu_rebuild();
$type_exists = db_query('SELECT 1 FROM {node_type} WHERE type = :type', array(':type' => $content_type_machine))->fetchField();
$this->assertTrue($type_exists, 'The new content type has been created in the database.');
-
- /*$table_schema = drupal_get_schema();
- $this->assertEqual(1, 1, print_r(array_keys($table_schema), TRUE));
- // Check the schema - the values should be in the per-type table.
- $this->assertSchemaMatchesTables(array(
- 'per_type' => array(
- $this->content_types[0]->type => array($field['field_name'] => array('url', 'title', 'attributes')),
- ),
- ));*/
}
}
diff --git a/sites/all/modules/contrib/fields/link/tests/link.crud_browser.test b/sites/all/modules/contrib/fields/link/tests/link.crud_browser.test
index 95406018..c90e86d1 100644
--- a/sites/all/modules/contrib/fields/link/tests/link.crud_browser.test
+++ b/sites/all/modules/contrib/fields/link/tests/link.crud_browser.test
@@ -76,8 +76,8 @@ class LinkUITest extends DrupalWebTestcase {
//$this->drupalGet('node/add');
$this->drupalGet('node/add/page');
$field_name = 'field_' . $name;
- $this->assertField('edit-field-'. $name .'-und-0-title', 'Title found');
- $this->assertField('edit-field-'. $name .'-und-0-url', 'URL found');
+ $this->assertField('edit-field-' . $name . '-und-0-title', 'Title found');
+ $this->assertField('edit-field-' . $name . '-und-0-url', 'URL found');
$input_test_cases = array(
array(
@@ -104,26 +104,32 @@ class LinkUITest extends DrupalWebTestcase {
'msg' => 'js label',
'type' => self::LINK_INPUT_TYPE_BAD_TITLE
),
- array(
+ array(
'href' => 'http://example.com/' . $this->randomName(),
'label' => $this->randomName() . '\' onmouseover="alert(\'hi\')',
'msg' => 'js label',
'type' => self::LINK_INPUT_TYPE_BAD_TITLE
),
- array(
+ array(
'href' => 'javascript:alert("http://example.com/' . $this->randomName() . '")',
'label' => $this->randomName(),
'msg' => 'js url',
'type' => self::LINK_INPUT_TYPE_BAD_URL
),
+ array(
+ 'href' => 'http://ecs-es.kelkoo.es/ctl/go/sitesearchGo?.ts=1338833010331&.sig=qP9GXeEFH6syBzwmzYkxmsvp1EI-',
+ 'label' => 'http://ecs-es.kelkoo.es/ctl/go/sitesearchGo?.ts=1338833010331&.sig=qP9GXeEFH6syBzwmzYkxmsvp1EI-',
+ 'msg' => 'Url with . in querystring',
+ 'type' => self::LINK_INPUT_TYPE_GOOD,
+ ),
);
$test_case = array(
- 'href' => 'www.example.com/'. $this->randomName(),
+ 'href' => 'www.example.com/' . $this->randomName(),
'label' => $this->randomName(),
'msg' => 'Link found',
'type' => self::LINK_INPUT_TYPE_GOOD,
);
- $test_case['expected_href'] = 'http://'. $test_case['href'];
+ $test_case['expected_href'] = 'http://' . $test_case['href'];
$input_test_cases[] = $test_case;
foreach ($input_test_cases as $input) {
@@ -137,17 +143,17 @@ class LinkUITest extends DrupalWebTestcase {
);
$this->drupalPost(NULL, $edit, t('Save'));
if ($input['type'] == self::LINK_INPUT_TYPE_BAD_URL) {
- $this->assertRaw(t('The value provided for %field is not a valid URL.', array('%field' => $name)), 'Not a valid URL: ' . $input['href']);
+ $this->assertRaw(t('The value %value provided for %field is not a valid URL.', array('%field' => $name, '%value' => trim($input['href']))), 'Not a valid URL: ' . $input['href']);
continue;
}
else {
- $this->assertRaw(t(' has been created.',
+ $this->assertRaw(' ' . t('has been created.',
array('@type' => 'Basic Page', '%title' => $edit['title'])),
'Page created: ' . $input['href']);
}
$url = $this->getUrl();
- // change to anonym user
+ // change to Anonymous user.
$this->drupalLogout();
$this->drupalGet($url);
@@ -156,12 +162,11 @@ class LinkUITest extends DrupalWebTestcase {
// us and let us know it's broken.
$this->assertFalse(libxml_use_internal_errors(TRUE));
if (isset($input['expected_href'])) {
- $path = '//a[@href="'. $input['expected_href'] .'" and text()="'. $input['label'] .'"]';
+ $path = '//a[@href="' . $input['expected_href'] . '" and text()="' . $input['label'] . '"]';
}
else {
- $path = '//a[@href="'. $input['href'] .'" and text()="'. $input['label'] .'"]';
+ $path = '//a[@href="' . $input['href'] . '" and text()="' . $input['label'] . '"]';
}
- //$this->pass(htmlentities($path));
$elements = $this->xpath($path);
libxml_use_internal_errors(FALSE);
$this->assertIdentical(isset($elements[0]), $input['type'] == self::LINK_INPUT_TYPE_GOOD, $input['msg']);
@@ -179,7 +184,7 @@ class LinkUITest extends DrupalWebTestcase {
// create field
$name = strtolower($this->randomName());
- $field_name = 'field_'. $name;
+ $field_name = 'field_' . $name;
$edit = array(
'fields[_add_new_field][label]' => $name,
'fields[_add_new_field][field_name]' => $name,
@@ -190,7 +195,7 @@ class LinkUITest extends DrupalWebTestcase {
$this->drupalPost(NULL, array(), t('Save field settings'));
$this->drupalPost(NULL, array(
'instance[settings][title]' => 'value',
- 'instance[settings][title_value]' => ''. $name .''), t('Save settings'));
+ 'instance[settings][title_value]' => '' . $name . ''), t('Save settings'));
// Is field created?
$this->assertRaw(t('Saved %label configuration', array('%label' => $name)), 'Field added');
@@ -215,7 +220,57 @@ class LinkUITest extends DrupalWebTestcase {
$this->drupalLogout();
$this->drupalGet($url);
- $this->assertRaw(l(''. $name .'', $input['href'], array('html' => TRUE)));
+ $this->assertRaw(l('' . $name . '', $input['href'], array('html' => TRUE)));
+ }
+
+ /**
+ * Testing that if you have the title but no url, the title is not sanitized twice.
+ */
+ function testCRUDTitleOnlyTitleNoLink() {
+ $this->web_user = $this->drupalCreateUser(array('administer content types', 'access content', 'create page content'));
+ $this->drupalLogin($this->web_user);
+
+ // create field
+ $name = strtolower($this->randomName());
+ $field_name = 'field_' . $name;
+ $edit = array(
+ 'fields[_add_new_field][label]' => $name,
+ 'fields[_add_new_field][field_name]' => $name,
+ 'fields[_add_new_field][type]' => 'link_field',
+ 'fields[_add_new_field][widget_type]' => 'link_field',
+ );
+ $this->drupalPost('admin/structure/types/manage/page/fields', $edit, t('Save'));
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+ $this->drupalPost(NULL, array(
+ 'instance[settings][url]' => 1,
+ ), t('Save settings'));
+
+ // Is field created?
+ $this->assertRaw(t('Saved %label configuration', array('%label' => $name)), 'Field added');
+
+ // create page form
+ $this->drupalGet('node/add/page');
+ $this->assertField($field_name . '[und][0][url]', 'URL found');
+
+ $input = array(
+ 'title' => 'This & That',
+ 'href' => '',
+ );
+
+ $edit = array(
+ 'title' => $name,
+ $field_name . '[und][0][title]' => $input['title'],
+ $field_name . '[und][0][url]' => $input['href'],
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ $url = $this->getUrl();
+
+ // change to anonymous user
+ $this->drupalLogout();
+ $this->drupalGet($url);
+
+ $this->assertRaw('This & That');
}
/**
@@ -242,25 +297,19 @@ class LinkUITest extends DrupalWebTestcase {
$this->assertRaw(t('Saved %label configuration', array('%label' => $name)), 'Field added');
node_types_rebuild();
menu_rebuild();
- //_content_type_info(TRUE);
- //$fields = content_fields();
- //$field = $fields['field_'. $name];
- //$field = field_info_field('field_'. $name);
+
_field_info_collate_fields(TRUE);
$instances = field_info_instances('node', 'page');
- //$this->debug($instances);
- //$this->assert('debug', ''. print_r($instances, TRUE) .'
', 'Debug');
- $instance = $instances['field_'. $name];
- //$this->assertTrue(1 === $instance['validate_url'], 'Make sure validation is on.');
+
+ $instance = $instances['field_' . $name];
$this->assertFalse($instance['required'], 'Make sure field is not required.');
$this->assertEqual($instance['settings']['title'], 'optional', 'Title should be optional by default.');
- $this->assertTrue($instance['settings']['enable_tokens'], 'Enable Tokens should be off by default.');
+ $this->assertTrue($instance['settings']['validate_url'], 'Make sure validation is on.');
+ $this->assertTrue($instance['settings']['enable_tokens'], 'Enable Tokens should be on by default.');
$this->assertEqual($instance['settings']['display']['url_cutoff'], 80, 'Url cutoff should be at 80 characters.');
$this->assertEqual($instance['settings']['attributes']['target'], 'default', 'Target should be "default"');
$this->assertFalse($instance['settings']['attributes']['rel'], 'Rel should be blank by default.');
$this->assertFalse($instance['settings']['attributes']['class'], 'By default, no class should be set.');
$this->assertFalse($instance['settings']['title_value'], 'By default, no title should be set.');
-
- //$this->fail(''. print_r($fields['field_'. $name], TRUE) .'
');
}
}
diff --git a/sites/all/modules/contrib/fields/link/tests/link.test b/sites/all/modules/contrib/fields/link/tests/link.test
index 538e3a67..962197f2 100644
--- a/sites/all/modules/contrib/fields/link/tests/link.test
+++ b/sites/all/modules/contrib/fields/link/tests/link.test
@@ -36,8 +36,8 @@ class LinkBaseTestClass extends DrupalWebTestCase {
'fields[_add_new_field][type]' => 'link_field',
'fields[_add_new_field][widget_type]' => 'link_field',
);
- $field_name = 'field_'. $name;
- $this->drupalPost('admin/structure/types/manage/'. $node_type .'/fields', $edit, t('Save'));
+ $field_name = 'field_' . $name;
+ $this->drupalPost('admin/structure/types/manage/' . $node_type . '/fields', $edit, t('Save'));
$this->drupalPost(NULL, array(), t('Save field settings'));
$this->drupalPost(NULL, $settings, t('Save settings'));
diff --git a/sites/all/modules/contrib/fields/link/tests/link.token.test b/sites/all/modules/contrib/fields/link/tests/link.token.test
index de3ed3fe..21bc4a04 100644
--- a/sites/all/modules/contrib/fields/link/tests/link.token.test
+++ b/sites/all/modules/contrib/fields/link/tests/link.token.test
@@ -75,7 +75,7 @@ class LinkTokenTest extends LinkBaseTestClass {
$name = $this->randomName();
$settings = array(
'instance[settings][title]' => 'value',
- 'instance[settings][title_value]' => $name .' [node:content-type:machine-name]');
+ 'instance[settings][title_value]' => $name . ' [node:content-type:machine-name]');
$field_name = $this->createLinkField('page', $settings);
// create page form
@@ -118,7 +118,7 @@ class LinkTokenTest extends LinkBaseTestClass {
$name = $this->randomName();
$settings = array(
'instance[settings][title]' => 'value',
- 'instance[settings][title_value]' => $name .' [node:title]');
+ 'instance[settings][title_value]' => $name . ' [node:title]');
$field_name = $this->createLinkField('page', $settings);
// create page form
@@ -144,7 +144,7 @@ class LinkTokenTest extends LinkBaseTestClass {
$this->drupalLogout();
$this->drupalGet($url);
- $this->assertRaw(l($name .' '. $name, $input['href']));
+ $this->assertRaw(l($name . ' ' . $name, $input['href']));
}
// This test doesn't seem to actually work, due to lack of 'title' in url.
@@ -191,9 +191,9 @@ class LinkTokenTest extends LinkBaseTestClass {
$edit = array();
$test_link_url = 'http://www.example.com/test';
- $edit[$field_name .'[und][0][url]'] = $test_link_url;
- $title = 'title_'. $this->randomName(20);
- $edit[$field_name .'[und][0][title]'] = $title;
+ $edit[$field_name . '[und][0][url]'] = $test_link_url;
+ $title = 'title_' . $this->randomName(20);
+ $edit[$field_name . '[und][0][title]'] = $title;
$edit['title'] = $name;
$this->drupalGet('node/add/page');
@@ -206,8 +206,8 @@ class LinkTokenTest extends LinkBaseTestClass {
//$this->drupalGet('node/'. $node->nid);
$this->assertText($title, 'Make sure the link title/text shows');
- $this->assertRaw(' title="'. $test_link_url .'"', "Do we show the link url as the title attribute?");
- $this->assertNoRaw(' title="['. $field_name .'-url]"');
+ $this->assertRaw(' title="' . $test_link_url . '"', "Do we show the link url as the title attribute?");
+ $this->assertNoRaw(' title="[' . $field_name . '-url]"');
$this->assertTrue(module_exists('token'), t('Assure that Token Module is enabled.'));
//$this->fail($this->content);
}
@@ -236,10 +236,10 @@ class LinkTokenTest extends LinkBaseTestClass {
$field_db_info = content_database_info($field);
$url_type = str_replace('_', '-', $this->content_types[0]->type);
- $edit = array('attributes[title]' => '['. $field_name .'-title]',
+ $edit = array('attributes[title]' => '[' . $field_name . '-title]',
'enable_tokens' => TRUE);
- $this->drupalPost('admin/content/node-type/'. $url_type .'/fields/'. $field['field_name'],
+ $this->drupalPost('admin/content/node-type/' . $url_type . '/fields/' . $field['field_name'],
$edit, t('Save field settings'));
$this->assertText(t('Saved field @field_name', array('@field_name' => $field['field_name'])));
@@ -248,14 +248,14 @@ class LinkTokenTest extends LinkBaseTestClass {
$node = node_load($this->nodes[0]->nid);
- $this->drupalGet('node/'. $this->nodes[0]->nid);
+ $this->drupalGet('node/' . $this->nodes[0]->nid);
$edit = array();
- $edit[$field['field_name'] .'[0][url]'] = 'http://www.example.com/test';
- $title = 'title_'. $this->randomName(20);
- $edit[$field['field_name'] .'[0][title]'] = $title;
+ $edit[$field['field_name'] . '[0][url]'] = 'http://www.example.com/test';
+ $title = 'title_' . $this->randomName(20);
+ $edit[$field['field_name'] . '[0][title]'] = $title;
- $this->drupalPost('node/'. $this->nodes[0]->nid .'/edit', $edit, t('Save'));
+ $this->drupalPost('node/' . $this->nodes[0]->nid . '/edit', $edit, t('Save'));
// Make sure we get a new version!
$node = node_load($this->nodes[0]->nid, NULL, TRUE);
@@ -263,10 +263,10 @@ class LinkTokenTest extends LinkBaseTestClass {
array('@title' => $node->title,
'@type' => $this->content_types[0]->name)));
- $this->drupalGet('node/'. $node->nid);
+ $this->drupalGet('node/' . $node->nid);
$this->assertText($title, 'Make sure the link title/text shows');
- $this->assertNoRaw(' title="'. $title .'"', "We should not show the link title as the title attribute?");
- $this->assertNoRaw(' title="['. $field_name .'-title]"');
+ $this->assertNoRaw(' title="' . $title . '"', "We should not show the link title as the title attribute?");
+ $this->assertNoRaw(' title="[' . $field_name . '-title]"');
//$this->fail($this->content);
}
@@ -319,7 +319,7 @@ class LinkTokenTest extends LinkBaseTestClass {
$this->drupalLogout();
$this->drupalGet($url);
- $this->assertRaw(l($input['label'], $input['href'] .'/page'));
+ $this->assertRaw(l($input['label'], $input['href'] . '/page'));
//$this->fail($this->content);
}
@@ -372,6 +372,58 @@ class LinkTokenTest extends LinkBaseTestClass {
$this->drupalLogout();
$this->drupalGet($url);
- $this->assertRaw(l($input['label'], $input['href'] .'/'. $this->web_user->uid));
+ $this->assertRaw(l($input['label'], $input['href'] . '/' . $this->web_user->uid));
+ }
+
+ /**
+ * Test that if you have a title and no url on a field which does not have tokens enabled,
+ * that the title is sanitized once.
+ */
+ function testCRUDTitleOnlyTitleNoLink2() {
+ $this->web_user = $this->drupalCreateUser(array('administer content types', 'access content', 'create page content'));
+ $this->drupalLogin($this->web_user);
+
+ // create field
+ $name = strtolower($this->randomName());
+ $field_name = 'field_' . $name;
+ $edit = array(
+ 'fields[_add_new_field][label]' => $name,
+ 'fields[_add_new_field][field_name]' => $name,
+ 'fields[_add_new_field][type]' => 'link_field',
+ 'fields[_add_new_field][widget_type]' => 'link_field',
+ );
+ $this->drupalPost('admin/structure/types/manage/page/fields', $edit, t('Save'));
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+ $this->drupalPost(NULL, array(
+ 'instance[settings][url]' => 1,
+ 'instance[settings][enable_tokens]' => 0,
+ ), t('Save settings'));
+
+ // Is field created?
+ $this->assertRaw(t('Saved %label configuration', array('%label' => $name)), 'Field added');
+
+ // create page form
+ $this->drupalGet('node/add/page');
+ $this->assertField($field_name . '[und][0][url]', 'URL found');
+
+ $input = array(
+ 'title' => 'This & That',
+ 'href' => '',
+ );
+
+ $edit = array(
+ 'title' => $name,
+ $field_name . '[und][0][title]' => $input['title'],
+ $field_name . '[und][0][url]' => $input['href'],
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ $url = $this->getUrl();
+
+ // change to anonymous user
+ $this->drupalLogout();
+ $this->drupalGet($url);
+
+ $this->assertRaw('This & That');
}
}
diff --git a/sites/all/modules/contrib/fields/link/tests/link.validate.test b/sites/all/modules/contrib/fields/link/tests/link.validate.test
index 0f721fbe..764eeaad 100644
--- a/sites/all/modules/contrib/fields/link/tests/link.validate.test
+++ b/sites/all/modules/contrib/fields/link/tests/link.validate.test
@@ -34,7 +34,7 @@ class LinkValidateTestCase extends LinkBaseTestClass {
$field_name . '[und][0][url]' => $url,
);
$this->drupalPost(NULL, $edit, t('Save'));
- $this->assertRaw(t(' has been created.'), 'Node created');
+ $this->assertRaw(' has been created.', 'Node created');
$nid = 1; //$matches[1];
@@ -90,17 +90,17 @@ class LinkValidateTest extends LinkValidateTestCase {
// create page form
$this->drupalGet('node/add/page');
$field_name = 'field_' . $name;
- $this->assertField('edit-field-'. $name .'-und-0-title', 'Title found');
- $this->assertField('edit-field-'. $name .'-und-0-url', 'URL found');
+ $this->assertField('edit-field-' . $name . '-und-0-title', 'Title found');
+ $this->assertField('edit-field-' . $name . '-und-0-url', 'URL found');
$edit = array(
'title' => 'Simple Title',
- $field_name .'[und][0][url]' => 'edik:naw',
+ $field_name . '[und][0][url]' => 'edik:naw',
);
$this->drupalPost(NULL, $edit, t('Save'));
- $this->assertText(t('The value provided for @field is not a valid URL.', array('@field' => $name)));
+ $this->assertText(t('The value @value provided for @field is not a valid URL.', array('@value' => 'edik:naw', '@field' => $name)));
}
/**
@@ -135,17 +135,17 @@ class LinkValidateTest extends LinkValidateTestCase {
// create page form
$this->drupalGet('node/add/page');
$field_name = 'field_' . $name;
- $this->assertField('edit-field-'. $name .'-und-0-title', 'Title found');
- $this->assertField('edit-field-'. $name .'-und-0-url', 'URL found');
+ $this->assertField('edit-field-' . $name . '-und-0-title', 'Title found');
+ $this->assertField('edit-field-' . $name . '-und-0-url', 'URL found');
$edit = array(
'title' => 'Simple Title',
- $field_name .'[und][0][url]' => 'edik:naw',
+ $field_name . '[und][0][url]' => 'edik:naw',
);
$this->drupalPost(NULL, $edit, t('Save'));
- $this->assertText(t('The value provided for @field is not a valid URL.', array('@field' => $name)));
+ $this->assertText(t('The value @value provided for @field is not a valid URL.', array('@field' => $name, '@value' => 'edik:naw')));
}
@@ -185,17 +185,17 @@ class LinkValidateTest extends LinkValidateTestCase {
// create page form
$this->drupalGet('node/add/page');
$field_name = 'field_' . $name;
- $this->assertField('edit-field-'. $name .'-und-0-title', 'Title found');
- $this->assertField('edit-field-'. $name .'-und-0-url', 'URL found');
+ $this->assertField('edit-field-' . $name . '-und-0-title', 'Title found');
+ $this->assertField('edit-field-' . $name . '-und-0-url', 'URL found');
$edit = array(
'title' => 'Simple Title',
- $field_name .'[und][0][url]' => 'edik:naw',
+ $field_name . '[und][0][url]' => 'edik:naw',
);
$this->drupalPost(NULL, $edit, t('Save'));
- $this->assertNoText(t('The value provided for @field is not a valid URL.', array('@field' => $name)));
+ $this->assertNoText(t('The value %value provided for %field is not a valid URL.', array('%field' => $name, '%value' => 'edik:naw')));
}
/**
@@ -207,10 +207,10 @@ class LinkValidateTest extends LinkValidateTestCase {
'administer nodes',
'access administration pages',
'access content',
- 'create '. $this->content_types[0]->type .' content',
- 'edit any '. $this->content_types[0]->type .' content'));
+ 'create ' . $this->content_types[0]->type . ' content',
+ 'edit any ' . $this->content_types[0]->type . ' content'));
$this->drupalLogin($this->web_user);
- variable_set('node_options_'. $this->content_types[0]->name, array('status', 'promote'));
+ variable_set('node_options_' . $this->content_types[0]->name, array('status', 'promote'));
$field_settings = array(
'type' => 'link',
'widget_type' => 'link',
@@ -227,23 +227,23 @@ class LinkValidateTest extends LinkValidateTestCase {
$node = node_load($this->nodes[0]->nid);
- $this->drupalGet('node/'. $this->nodes[0]->nid);
+ $this->drupalGet('node/' . $this->nodes[0]->nid);
$edit = array();
$title = $this->randomName();
$url = 'javascript:alert("http://example.com/' . $this->randomName() . '")';
- $edit[$field['field_name'] .'[0][url]'] = $url;
- $edit[$field['field_name'] .'[0][title]'] = $title;
+ $edit[$field['field_name'] . '[0][url]'] = $url;
+ $edit[$field['field_name'] . '[0][title]'] = $title;
- $this->drupalPost('node/'. $this->nodes[0]->nid .'/edit', $edit, t('Save'));
+ $this->drupalPost('node/' . $this->nodes[0]->nid . '/edit', $edit, t('Save'));
//$this->pass($this->content);
- $this->assertNoText(t('The value provided for %field is not a valid URL.', array('%field' => $name)));
+ $this->assertNoText(t('The value %value provided for %field is not a valid URL.', array('%field' => $name, '%value' => trim($url))));
// Make sure we get a new version!
$node = node_load($this->nodes[0]->nid, NULL, TRUE);
$this->assertEqual($url, $node->{$field['field_name']}[0]['url']);
- $this->drupalGet('node/'. $node->nid);
+ $this->drupalGet('node/' . $node->nid);
$this->assertNoRaw($url, 'Make sure Javascript does not display.');
// Turn the array validation back _on_.
@@ -251,9 +251,9 @@ class LinkValidateTest extends LinkValidateTestCase {
$node_type_link = str_replace('_', '-', $node->type);
//$this->drupalGet('admin/content/node-type/'. $node_type_link .'/fields'); ///'. $field['field_name']);
//$this->fail($this->content);
- $this->drupalPost('admin/content/node-type/'. $node_type_link .'/fields/'. $field['field_name'], $edit, t('Save field settings'));
+ $this->drupalPost('admin/content/node-type/' . $node_type_link . '/fields/' . $field['field_name'], $edit, t('Save field settings'));
- $this->drupalGet('node/'. $node->nid);
+ $this->drupalGet('node/' . $node->nid);
// This actually works because the display_url goes through the core
// url() function. But we should have a test that makes sure it continues
// to work.
@@ -382,7 +382,7 @@ class LinkValidateUrlLight extends DrupalWebTestCase {
case FALSE:
return "Invalid Link";
default:
- return "Bad Value:". $type;
+ return "Bad Value:" . $type;
}
}
@@ -460,12 +460,12 @@ class LinkValidateUrlLight extends DrupalWebTestCase {
$allowed_protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'));
foreach ($allowed_protocols as $protocol) {
if ($protocol !== 'news' && $protocol !== 'mailto') {
- $links[] = $protocol .'://www.example.com';
+ $links[] = $protocol . '://www.example.com';
}
}
foreach ($links as $link) {
$valid = link_validate_url($link);
- $this->assertEqual(LINK_EXTERNAL, $valid, 'Testing that '. $link .' is a valid external link.');
+ $this->assertEqual(LINK_EXTERNAL, $valid, 'Testing that ' . $link . ' is a valid external link.');
// The following two lines are commented out and only used for comparisons.
//$valid2 = valid_url($link, TRUE);
//$this->assertEqual(TRUE, $valid2, "Using valid_url() on $link.");
@@ -488,7 +488,7 @@ class LinkValidateUrlLight extends DrupalWebTestCase {
);
foreach ($links as $link) {
$valid = link_validate_url($link);
- $this->assertEqual(FALSE, $valid, 'Testing that '. $link .' is not a valid link.');
+ $this->assertEqual(FALSE, $valid, 'Testing that ' . $link . ' is not a valid link.');
}
}
}
diff --git a/sites/all/modules/contrib/fields/link/views/link_views_handler_argument_target.inc b/sites/all/modules/contrib/fields/link/views/link_views_handler_argument_target.inc
index 3a6fdf01..f0622d05 100644
--- a/sites/all/modules/contrib/fields/link/views/link_views_handler_argument_target.inc
+++ b/sites/all/modules/contrib/fields/link/views/link_views_handler_argument_target.inc
@@ -144,6 +144,6 @@ class link_views_handler_argument_target extends views_handler_argument {
$this->ensure_my_table();
// Because attributes are stored serialized, our only option is to also
// serialize the data we're searching for and use LIKE to find similar data.
- $this->query->add_where(0, $this->table_alias . '.' . $this->real_field . " LIKE '%%%s%'", serialize(array('target' => $this->argument)));
+ $this->query->add_where(0, $this->table_alias . ' . ' . $this->real_field . " LIKE '%%%s%'", serialize(array('target' => $this->argument)));
}
}
diff --git a/sites/all/modules/contrib/fields/link/views/link_views_handler_filter_protocol.inc b/sites/all/modules/contrib/fields/link/views/link_views_handler_filter_protocol.inc
index 9d194aa9..a020b4e0 100644
--- a/sites/all/modules/contrib/fields/link/views/link_views_handler_filter_protocol.inc
+++ b/sites/all/modules/contrib/fields/link/views/link_views_handler_filter_protocol.inc
@@ -90,12 +90,12 @@ class link_views_handler_filter_protocol extends views_handler_filter_string {
// PostGreSQL code has NOT been tested. Please report any problems to the link issue queue.
// pgSQL requires all slashes to be double escaped in regular expressions.
// See http://www.postgresql.org/docs/8.1/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
- $condition .= ' OR ' . $field .' ~* \''.'^(([a-z0-9]([a-z0-9\\-_]*\\.)+)(' . $LINK_DOMAINS . '|[a-z][a-z]))' . '\'';
+ $condition .= ' OR ' . $field . ' ~* \'' . '^(([a-z0-9]([a-z0-9\\-_]*\\.)+)(' . $LINK_DOMAINS . '|[a-z][a-z]))' . '\'';
}
else {
// mySQL requires backslashes to be double (triple?) escaped within character classes.
// See http://dev.mysql.com/doc/refman/5.0/en/string-comparison-functions.html#operator_regexp
- $condition .= ' OR ' . $field . ' REGEXP \''.'^(([a-z0-9]([a-z0-9\\\-_]*\.)+)(' . $LINK_DOMAINS . '|[a-z][a-z]))' . '\'';
+ $condition .= ' OR ' . $field . ' REGEXP \'' . '^(([a-z0-9]([a-z0-9\\\-_]*\.)+)(' . $LINK_DOMAINS . '|[a-z][a-z]))' . '\'';
}
}
diff --git a/sites/all/modules/contrib/files/imce/imce/imce.info b/sites/all/modules/contrib/files/imce/imce/imce.info
index 60b6ac8c..9fd852c5 100644
--- a/sites/all/modules/contrib/files/imce/imce/imce.info
+++ b/sites/all/modules/contrib/files/imce/imce/imce.info
@@ -4,9 +4,9 @@ core = "7.x"
package = "Media"
configure = "admin/config/media/imce"
-; Information added by drupal.org packaging script on 2013-01-29
-version = "7.x-1.7"
+; Information added by Drupal.org packaging script on 2014-05-16
+version = "7.x-1.9"
core = "7.x"
project = "imce"
-datestamp = "1359476607"
+datestamp = "1400275428"
diff --git a/sites/all/modules/contrib/files/imce/imce/imce.install b/sites/all/modules/contrib/files/imce/imce/imce.install
index 688c6b5b..40ddcd3f 100644
--- a/sites/all/modules/contrib/files/imce/imce/imce.install
+++ b/sites/all/modules/contrib/files/imce/imce/imce.install
@@ -101,4 +101,24 @@ function imce_update_7001(&$sandbox) {
db_drop_table('imce_files');
return t('Migrated IMCE files.');
}
+}
+
+/**
+ * Fixes misconfigurations where anonymous user is given User-1 profile
+ */
+function imce_update_7002() {
+ $roles = variable_get('imce_roles_profiles', array());
+ $rid = DRUPAL_ANONYMOUS_RID;
+ if (!empty($roles[$rid])) {
+ $update = FALSE;
+ foreach ($roles[$rid] as $key => $value) {
+ if ($value == 1 && substr($key, -4) == '_pid') {
+ $roles[$rid][$key] = '0';
+ $update = TRUE;
+ }
+ }
+ if ($update) {
+ variable_set('imce_roles_profiles', $roles);
+ }
+ }
}
\ No newline at end of file
diff --git a/sites/all/modules/contrib/files/imce/imce/inc/imce.admin.inc b/sites/all/modules/contrib/files/imce/imce/inc/imce.admin.inc
index ff496183..c2d3625c 100644
--- a/sites/all/modules/contrib/files/imce/imce/inc/imce.admin.inc
+++ b/sites/all/modules/contrib/files/imce/imce/inc/imce.admin.inc
@@ -16,7 +16,8 @@ function imce_admin() {
$rows = array();
foreach ($profiles as $pid => $profile) {
- $rows[] = array($profile['name'],
+ $rows[] = array(
+ check_plain($profile['name']),
l(t('Edit'), 'admin/config/media/imce/profile/edit/' . $pid),
$pid == 1 ? '' : l(t('Delete'), 'admin/config/media/imce/profile/delete/' . $pid),
);
@@ -34,6 +35,18 @@ function imce_admin() {
'#attributes' => array('id' => 'imce-profiles-list'),
);
$output['form'] = drupal_get_form('imce_admin_form');
+
+ // Display security warnings
+ if (empty($_POST)) {
+ $roles = variable_get('imce_roles_profiles', array());
+ if (!empty($roles[DRUPAL_ANONYMOUS_RID]['public_pid']) || !empty($roles[DRUPAL_ANONYMOUS_RID]['private_pid'])) {
+ drupal_set_message(t('Anonymous user role has access to IMCE.') . ' ' . t('Make sure this is not a misconfiguration.'), 'warning');
+ }
+ if (imce_admin_check_wildcard_upload(DRUPAL_AUTHENTICATED_RID, $roles)) {
+ drupal_set_message(t('Authenticated user role is assigned a configuration profile with unrestricted file extensions.') . ' ' . t('Make sure this is not a misconfiguration.'), 'warning');
+ }
+ }
+
return $output;
}
@@ -116,7 +129,7 @@ function imce_admin_theme($variables) {
$swrappers = file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE);
foreach ($swrappers as $scheme => $info) {
$header[] = l($info['name'], 'imce/' . $scheme);
- $rows[0][] = $profile1['name'];
+ $rows[0][] = check_plain($profile1['name']);
$keys[] = $scheme . '_pid';
}
@@ -145,6 +158,17 @@ function imce_admin_theme($variables) {
return $output;
}
+/**
+ * Validate admin form.
+ */
+function imce_admin_form_validate($form, &$form_state) {
+ $roles = $form_state['values']['roles'];
+ // Check anonymous profile. Do not allow wildcard upload.
+ if ($key = imce_admin_check_wildcard_upload(DRUPAL_ANONYMOUS_RID, $roles)) {
+ form_error($form['roles'][DRUPAL_ANONYMOUS_RID][$key], t('Anonymous user role can not have a configuration profile with unrestricted file extensions.'));
+ }
+}
+
/**
* Submit admin form.
*/
@@ -172,7 +196,7 @@ function imce_profile_operations($op = 'add', $pid = 0) {
return drupal_get_form('imce_profile_delete_form', $pid);
}
//add-edit
- if ($pid != 1 || $GLOBALS['user']->uid == 1) {
+ if ($op === 'add' || $op === 'edit') {
return drupal_get_form('imce_profile_form', $pid);
}
drupal_access_denied();
@@ -482,7 +506,7 @@ function imce_thumbnails_theme($variables) {
*/
function imce_role_form($role, $weight = TRUE, $core = TRUE) {
$form['name'] = array(
- '#markup' => $role['name'],
+ '#markup' => check_plain($role['name']),
);
if ($weight) {
$form['weight'] = $core ? array(
@@ -687,5 +711,30 @@ function imce_rolesort($r1, $r2) {
return $r1['weight']-$r2['weight'];
}
+/**
+ * Checks if the given role can upload all extensions.
+ */
+function imce_admin_check_wildcard_upload($rid, $conf = NULL) {
+ if (!isset($conf)) {
+ $conf = variable_get('imce_roles_profiles', array());
+ }
+ if (!empty($conf[$rid])) {
+ foreach ($conf[$rid] as $key => $pid) {
+ if ($pid && substr($key, -4) == '_pid') {
+ if ($profile = imce_load_profile($pid)) {
+ if ($profile['extensions'] === '*' && !empty($profile['directories'])) {
+ foreach ($profile['directories'] as $dirconf) {
+ if (!empty($dirconf['upload'])) {
+ return $key;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return FALSE;
+}
+
//Include core profile functions.
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'imce') . '/inc/imce.core.profiles.inc';
\ No newline at end of file
diff --git a/sites/all/modules/contrib/files/imce/imce/inc/imce.page.inc b/sites/all/modules/contrib/files/imce/imce/inc/imce.page.inc
index bcb00b2d..ab52aa78 100644
--- a/sites/all/modules/contrib/files/imce/imce/inc/imce.page.inc
+++ b/sites/all/modules/contrib/files/imce/imce/inc/imce.page.inc
@@ -116,7 +116,7 @@ function imce_js($user, $scheme, $jsop = '') {
//disable devel log.
$GLOBALS['devel_shutdown'] = FALSE;
//for upload we must return plain text header.
- drupal_add_http_header('Content-Type', 'text/' . ($jsop == 'upload' ? 'html' : 'javascript') . '; charset=utf-8');
+ drupal_add_http_header('Content-Type', (!empty($_POST['html_response']) ? 'text/html' : 'application/json') . '; charset=utf-8');
print drupal_json_encode($response);
exit();
}
@@ -144,6 +144,7 @@ function imce_upload_form($form, &$form_state, $ref) {
'#submit' => $imce['perm']['upload'] ? array('imce_upload_submit') : NULL,
);
$form = array('fset_upload' => array('#type' => 'fieldset', '#title' => t('Upload file')) + $form);
+ $form['html_response'] = array('#type' => 'hidden', '#default_value' => '1');
$form['#attributes']['enctype'] = 'multipart/form-data';
$form['#action'] = $imce['url'];
return $form;
diff --git a/sites/all/modules/contrib/files/imce/imce/js/imce.js b/sites/all/modules/contrib/files/imce/imce/js/imce.js
index f0380873..4e8948d0 100644
--- a/sites/all/modules/contrib/files/imce/imce/js/imce.js
+++ b/sites/all/modules/contrib/files/imce/imce/js/imce.js
@@ -1,4 +1,3 @@
-
(function($) {
//Global container.
window.imce = {tree: {}, findex: [], fids: {}, selected: {}, selcount: 0, ops: {}, cache: {}, urlId: {},
@@ -9,6 +8,7 @@ hooks: {load: [], list: [], navigate: [], cache: []},
initiate: function() {
imce.conf = Drupal.settings.imce || {};
if (imce.conf.error != false) return;
+ imce.ie = (navigator.userAgent.match(/msie (\d+)/i) || ['', 0])[1] * 1;
imce.FLW = imce.el('file-list-wrapper'), imce.SBW = imce.el('sub-browse-wrapper');
imce.NW = imce.el('navigation-wrapper'), imce.BW = imce.el('browse-wrapper');
imce.PW = imce.el('preview-wrapper'), imce.FW = imce.el('forms-wrapper');
@@ -19,6 +19,8 @@ initiate: function() {
imce.initiateList();//process file list
imce.initiateOps();//prepare operation tabs
imce.refreshOps();
+ // Bind global error handler
+ $(document).ajaxError(imce.ajaxError);
imce.invoke('load', window);//run functions set by external applications.
},
@@ -79,7 +81,7 @@ dirCollapsible: function (branch) {
if (branch.ul) {
$(branch.ul).toggle();
$(branch.li).toggleClass('expanded');
- $.browser.msie && $('#navigation-header').css('top', imce.NW.scrollTop);
+ imce.ie && $('#navigation-header').css('top', imce.NW.scrollTop);
}
else if (branch.clkbl){
$(branch.a).click();
@@ -241,12 +243,19 @@ setHtmlOps: function () {
//convert upload form to an op.
setUploadOp: function () {
- var form = imce.el('imce-upload-form');
+ var el, form = imce.el('imce-upload-form');
if (!form) return;
$(form).ajaxForm(imce.uploadSettings()).find('fieldset').each(function() {//clean up fieldsets
this.removeChild(this.firstChild);
$(this).after(this.childNodes);
}).remove();
+ // Set html response flag
+ el = form.elements['files[imce]'];
+ if (el && el.files && window.FormData) {
+ if (el = form.elements.html_response) {
+ el.value = 0;
+ }
+ }
imce.opAdd({name: 'upload', title: Drupal.t('Upload'), content: form});//add op
},
@@ -257,7 +266,7 @@ setFileOps: function () {
$(form.elements.filenames).parent().remove();
$(form).find('fieldset').each(function() {//remove fieldsets
var $sbmt = $('input:submit', this);
- if (!$sbmt.size()) return;
+ if (!$sbmt.length) return;
var Op = {name: $sbmt.attr('id').substr(5)};
var func = function() {imce.fopSubmit(Op.name); return false;};
$sbmt.click(func);
@@ -279,7 +288,7 @@ refreshOps: function() {
//add a new file operation
opAdd: function (op) {
var oplist = imce.el('ops-list'), opcons = imce.el('op-contents');
- var name = op.name || ('op-'+ $(oplist).children('li').size());
+ var name = op.name || ('op-'+ $(oplist).children('li').length);
var title = op.title || 'Untitled';
var Op = imce.ops[name] = {title: title};
if (op.content) {
@@ -323,7 +332,7 @@ opClick: function(name) {
var $inputs = $('input', imce.ops[imce.vars.op].div);
$inputs.eq(0).focus();
//form inputs become invisible in IE. Solution is as stupid as the behavior.
- $('html').is('.ie') && $inputs.addClass('dummyie').removeClass('dummyie');
+ $('html').hasClass('ie') && $inputs.addClass('dummyie').removeClass('dummyie');
}
});
});
@@ -432,7 +441,6 @@ uploadValidate: function (data, form, options) {
return imce.setMessage(Drupal.t('Only files with the following extensions are allowed: %files-allowed.', {'%files-allowed': imce.conf.extensions}), 'error');
}
}
- var sep = path.indexOf('/') == -1 ? '\\' : '/';
options.url = imce.ajaxURL('upload');//make url contain current dir.
imce.fopLoading('upload', true);
return true;
@@ -440,7 +448,19 @@ uploadValidate: function (data, form, options) {
//settings for upload
uploadSettings: function () {
- return {beforeSubmit: imce.uploadValidate, success: function (response) {imce.processResponse($.parseJSON(response));}, complete: function () {imce.fopLoading('upload', false);}, resetForm: true};
+ return {
+ beforeSubmit: imce.uploadValidate,
+ success: function (response) {
+ try{
+ imce.processResponse($.parseJSON(response));
+ } catch(e) {}
+ },
+ complete: function () {
+ imce.fopLoading('upload', false);
+ },
+ resetForm: true,
+ dataType: 'text'
+ };
},
//validate default ops(delete, thumb, resize)
@@ -450,7 +470,7 @@ fopValidate: function(fop) {
case 'delete':
return confirm(Drupal.t('Delete selected files?'));
case 'thumb':
- if (!$('input:checked', imce.ops['thumb'].div).size()) {
+ if (!$('input:checked', imce.ops['thumb'].div).length) {
return imce.setMessage(Drupal.t('Please select a thumbnail.'), 'error');
}
return imce.validateImage();
@@ -487,7 +507,7 @@ commonSubmit: function(fop) {
//settings for default file operations
fopSettings: function (fop) {
- return {url: imce.ajaxURL(fop), type: 'POST', dataType: 'json', success: imce.processResponse, complete: function (response) {imce.fopLoading(fop, false);}, data: imce.vars.opform +'&filenames='+ imce.serialNames() +'&jsop='+ fop + (imce.ops[fop].div ? '&'+ $('input, select, textarea', imce.ops[fop].div).serialize() : '')};
+ return {url: imce.ajaxURL(fop), type: 'POST', dataType: 'json', success: imce.processResponse, complete: function (response) {imce.fopLoading(fop, false);}, data: imce.vars.opform +'&filenames='+ encodeURIComponent(imce.serialNames()) +'&jsop='+ fop + (imce.ops[fop].div ? '&'+ $('input, select, textarea', imce.ops[fop].div).serialize() : '')};
},
//toggle loading state
@@ -537,7 +557,7 @@ prepareMsgs: function () {
$('>div', msgs).each(function (){
var type = this.className.split(' ')[1];
var li = $('>ul li', this);
- if (li.size()) li.each(function () {imce.setMessage(this.innerHTML, type);});
+ if (li.length) li.each(function () {imce.setMessage(this.innerHTML, type);});
else imce.setMessage(this.innerHTML, type);
});
$(msgs).remove();
@@ -704,7 +724,10 @@ processRow: function (row) {
//decode urls. uses unescape. can be overridden to use decodeURIComponent
decode: function (str) {
- return unescape(str);
+ try {
+ return decodeURIComponent(str);
+ } catch(e) {}
+ return str;
},
//decode and convert to plain text
@@ -772,7 +795,7 @@ updateUI: function() {
return false;
}).appendTo('#op-contents')[0];
//navigation-header
- if (!$('#navigation-header').size()) {
+ if (!$('#navigation-header').length) {
$(imce.NW).children('.navigation-text').attr('id', 'navigation-header').wrapInner('');
}
//log
@@ -789,7 +812,7 @@ updateUI: function() {
imce.opAdd({name: 'help', title: $('#help-box-title').remove().text(), content: $('#help-box').show()});
});
//add ie classes
- $.browser.msie && $('html').addClass('ie') && parseFloat($.browser.version) < 8 && $('html').addClass('ie-7');
+ imce.ie && $('html').addClass('ie') && imce.ie < 8 && $('html').addClass('ie-7');
// enable box view for file list
imce.vars.boxW && imce.boxView();
//scrolling file list
@@ -802,6 +825,6 @@ updateUI: function() {
};
//initiate
-$(document).ready(imce.initiate).ajaxError(imce.ajaxError);
+$(document).ready(imce.initiate);
})(jQuery);
\ No newline at end of file
diff --git a/sites/all/modules/contrib/files/imce/imce/js/imce_extras.js b/sites/all/modules/contrib/files/imce/imce/js/imce_extras.js
index bccf1c1d..b558cc50 100644
--- a/sites/all/modules/contrib/files/imce/imce/js/imce_extras.js
+++ b/sites/all/modules/contrib/files/imce/imce/js/imce_extras.js
@@ -180,7 +180,7 @@ imce.setResizer = function (resizer, axis, area1, area2, Min, callback) {
//get&set area dimensions of the last session from the cookie
imce.recallDimensions = function() {
var $body = $(document.body);
- if (!$body.is('.imce')) return;
+ if (!$body.hasClass('imce')) return;
//row heights
imce.recallHeights(imce.cookie('imcebwh') * 1);
$(window).resize(function(){imce.recallHeights()});
@@ -192,7 +192,7 @@ imce.recallDimensions = function() {
//set row heights with respect to window height
imce.recallHeights = function(bwFixedHeight) {
//window & body dimensions
- var winHeight = $.browser.opera ? window.innerHeight : $(window).height();
+ var winHeight = window.opera ? window.innerHeight : $(window).height();
var bodyHeight = $(document.body).outerHeight(true);
var diff = winHeight - bodyHeight;
var bwHeight = $(imce.BW).height(), pwHeight = $(imce.PW).height();
@@ -214,9 +214,9 @@ imce.recallHeights = function(bwFixedHeight) {
//cookie get & set
imce.cookie = function (name, value) {
if (typeof(value) == 'undefined') {//get
- return unescape((document.cookie.match(new RegExp('(^|;) *'+ name +'=([^;]*)(;|$)')) || ['', '', ''])[2]);
+ return document.cookie ? imce.decode((document.cookie.match(new RegExp('(?:^|;) *' + name + '=([^;]*)(?:;|$)')) || ['', ''])[1].replace(/\+/g, '%20')) : '';
}
- document.cookie = name +'='+ escape(value) +'; expires='+ (new Date(new Date() * 1 + 15 * 86400000)).toGMTString() +'; path=' + Drupal.settings.basePath + 'imce';//set
+ document.cookie = name +'='+ encodeURIComponent(value) +'; expires='+ (new Date(new Date() * 1 + 15 * 86400000)).toUTCString() +'; path=' + Drupal.settings.basePath + 'imce';//set
};
//view thumbnails(smaller than tMaxW x tMaxH) inside the rows.
@@ -259,12 +259,12 @@ imce.imagestyleURL = function (url, stylename) {
// replace table view with box view for file list
imce.boxView = function () {
var w = imce.vars.boxW, h = imce.vars.boxH;
- if (!w || !h || $.browser.msie && parseFloat($.browser.version) < 8) return;
+ if (!w || !h || imce.ie && imce.ie < 8) return;
var $body = $(document.body);
var toggle = function() {
$body.toggleClass('box-view');
// refresh dom. required by all except FF.
- !$.browser.mozilla && $('#file-list').appendTo(imce.FW).appendTo(imce.FLW);
+ $('#file-list').appendTo(imce.FW).appendTo(imce.FLW);
};
$body.append('');
imce.hooks.load.push(function() {
diff --git a/sites/all/modules/contrib/files/imce/imce/js/imce_set_app.js b/sites/all/modules/contrib/files/imce/imce/js/imce_set_app.js
index 23908688..419f7df6 100644
--- a/sites/all/modules/contrib/files/imce/imce/js/imce_set_app.js
+++ b/sites/all/modules/contrib/files/imce/imce/js/imce_set_app.js
@@ -53,7 +53,7 @@ imce.hooks.load.push(function(win) {
if (appFields.url.indexOf(',') > -1) {
var arr = appFields.url.split(',');
for (var i in arr) {
- if ($('#'+ arr[i], appWindow.document).size()) {
+ if ($('#'+ arr[i], appWindow.document).length) {
appFields.url = arr[i];
break;
}
diff --git a/sites/all/modules/contrib/files/imce/imce/js/imce_set_inline.js b/sites/all/modules/contrib/files/imce/imce/js/imce_set_inline.js
index 82706b74..08891a10 100644
--- a/sites/all/modules/contrib/files/imce/imce/js/imce_set_inline.js
+++ b/sites/all/modules/contrib/files/imce/imce/js/imce_set_inline.js
@@ -21,7 +21,7 @@ Drupal.behaviors.imceInline = {attach: function(context, settings) {
//function to be executed when imce loads.
ii.load = function(win) {
win.imce.setSendTo(Drupal.t('Insert file'), ii.insert);
- $(window).unload(function() {
+ $(window).bind('unload', function() {
if (ii.pop && !ii.pop.closed) ii.pop.close();
});
};
diff --git a/sites/all/modules/contrib/files/imce/imce/js/jquery.form.js b/sites/all/modules/contrib/files/imce/imce/js/jquery.form.js
index 69bf8997..b750db69 100644
--- a/sites/all/modules/contrib/files/imce/imce/js/jquery.form.js
+++ b/sites/all/modules/contrib/files/imce/imce/js/jquery.form.js
@@ -1,6 +1,31 @@
-/*
+/*!
* jQuery Form Plugin
- * version: 2.67 (12-MAR-2011)
+ * version: 3.17 (25-SEP-2012)
* @requires jQuery v1.3.2 or later
*/
-(function(b){b.fn.ajaxSubmit=function(t){if(!this.length){a("ajaxSubmit: skipping submit process - no element selected");return this}if(typeof t=="function"){t={success:t}}var h=this.attr("action");var d=(typeof h==="string")?b.trim(h):"";if(d){d=(d.match(/^([^#]+)/)||[])[1]}d=d||window.location.href||"";t=b.extend(true,{url:d,type:this[0].getAttribute("method")||"GET",iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},t);var u={};this.trigger("form-pre-serialize",[this,t,u]);if(u.veto){a("ajaxSubmit: submit vetoed via form-pre-serialize trigger");return this}if(t.beforeSerialize&&t.beforeSerialize(this,t)===false){a("ajaxSubmit: submit aborted via beforeSerialize callback");return this}var f,p,m=this.formToArray(t.semantic);if(t.data){t.extraData=t.data;for(f in t.data){if(t.data[f] instanceof Array){for(var i in t.data[f]){m.push({name:f,value:t.data[f][i]})}}else{p=t.data[f];p=b.isFunction(p)?p():p;m.push({name:f,value:p})}}}if(t.beforeSubmit&&t.beforeSubmit(m,this,t)===false){a("ajaxSubmit: submit aborted via beforeSubmit callback");return this}this.trigger("form-submit-validate",[m,this,t,u]);if(u.veto){a("ajaxSubmit: submit vetoed via form-submit-validate trigger");return this}var c=b.param(m);if(t.type.toUpperCase()=="GET"){t.url+=(t.url.indexOf("?")>=0?"&":"?")+c;t.data=null}else{t.data=c}var s=this,l=[];if(t.resetForm){l.push(function(){s.resetForm()})}if(t.clearForm){l.push(function(){s.clearForm()})}if(!t.dataType&&t.target){var r=t.success||function(){};l.push(function(n){var k=t.replaceTarget?"replaceWith":"html";b(t.target)[k](n).each(r,arguments)})}else{if(t.success){l.push(t.success)}}t.success=function(w,n,x){var v=t.context||t;for(var q=0,k=l.length;q0;var e="multipart/form-data";var j=(s.attr("enctype")==e||s.attr("encoding")==e);if(t.iframe!==false&&(g||t.iframe||j)){if(t.closeKeepAlive){b.get(t.closeKeepAlive,o)}else{o()}}else{b.ajax(t)}this.trigger("form-submit-notify",[this,t]);return this;function o(){var v=s[0];if(b(":input[name=submit],:input[id=submit]",v).length){alert('Error: Form elements must not have name or id of "submit".');return}var B=b.extend(true,{},b.ajaxSettings,t);B.context=B.context||B;var E="jqFormIO"+(new Date().getTime()),z="_"+E;var w=b('');var A=w[0];w.css({position:"absolute",top:"-1000px",left:"-1000px"});var x={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(){a("aborting upload...");var n="aborted";this.aborted=1;w.attr("src",B.iframeSrc);x.error=n;B.error&&B.error.call(B.context,x,"error",n);I&&b.event.trigger("ajaxError",[x,B,n]);B.complete&&B.complete.call(B.context,x,"error")}};var I=B.global;if(I&&!b.active++){b.event.trigger("ajaxStart")}if(I){b.event.trigger("ajaxSend",[x,B])}if(B.beforeSend&&B.beforeSend.call(B.context,x,B)===false){if(B.global){b.active--}return}if(x.aborted){return}var H=0;var y=v.clk;if(y){var F=y.name;if(F&&!y.disabled){B.extraData=B.extraData||{};B.extraData[F]=y.value;if(y.type=="image"){B.extraData[F+".x"]=v.clk_x;B.extraData[F+".y"]=v.clk_y}}}function G(){var O=s.attr("target"),M=s.attr("action");v.setAttribute("target",E);if(v.getAttribute("method")!="POST"){v.setAttribute("method","POST")}if(v.getAttribute("action")!=B.url){v.setAttribute("action",B.url)}if(!B.skipEncodingOverride){s.attr({encoding:"multipart/form-data",enctype:"multipart/form-data"})}if(B.timeout){setTimeout(function(){H=true;D()},B.timeout)}var N=[];try{if(B.extraData){for(var P in B.extraData){N.push(b('').appendTo(v)[0])}}w.appendTo("body");A.attachEvent?A.attachEvent("onload",D):A.addEventListener("load",D,false);v.submit()}finally{v.setAttribute("action",M);if(O){v.setAttribute("target",O)}else{s.removeAttr("target")}b(N).remove()}}if(B.forceSync){G()}else{setTimeout(G,10)}var K,L,J=50;function D(){if(x.aborted){return}var R=A.contentWindow?A.contentWindow.document:A.contentDocument?A.contentDocument:A.document;if(!R||R.location.href==B.iframeSrc){return}A.detachEvent?A.detachEvent("onload",D):A.removeEventListener("load",D,false);var N=true;try{if(H){throw"timeout"}var S=B.dataType=="xml"||R.XMLDocument||b.isXMLDoc(R);a("isXml="+S);if(!S&&window.opera&&(R.body==null||R.body.innerHTML=="")){if(--J){a("requeing onLoad callback, DOM not available");setTimeout(D,250);return}}x.responseText=R.body?R.body.innerHTML:R.documentElement?R.documentElement.innerHTML:null;x.responseXML=R.XMLDocument?R.XMLDocument:R;x.getResponseHeader=function(U){var T={"content-type":B.dataType};return T[U]};var Q=/(json|script)/.test(B.dataType);if(Q||B.textarea){var M=R.getElementsByTagName("textarea")[0];if(M){x.responseText=M.value}else{if(Q){var P=R.getElementsByTagName("pre")[0];var n=R.getElementsByTagName("body")[0];if(P){x.responseText=P.textContent}else{if(n){x.responseText=n.innerHTML}}}}}else{if(B.dataType=="xml"&&!x.responseXML&&x.responseText!=null){x.responseXML=C(x.responseText)}}K=k(x,B.dataType,B)}catch(O){a("error caught:",O);N=false;x.error=O;B.error&&B.error.call(B.context,x,"error",O);I&&b.event.trigger("ajaxError",[x,B,O])}if(x.aborted){a("upload aborted");N=false}if(N){B.success&&B.success.call(B.context,K,"success",x);I&&b.event.trigger("ajaxSuccess",[x,B])}I&&b.event.trigger("ajaxComplete",[x,B]);if(I&&!--b.active){b.event.trigger("ajaxStop")}B.complete&&B.complete.call(B.context,x,N?"success":"error");setTimeout(function(){w.removeData("form-plugin-onload");w.remove();x.responseXML=null},100)}var C=b.parseXML||function(n,M){if(window.ActiveXObject){M=new ActiveXObject("Microsoft.XMLDOM");M.async="false";M.loadXML(n)}else{M=(new DOMParser()).parseFromString(n,"text/xml")}return(M&&M.documentElement&&M.documentElement.nodeName!="parsererror")?M:null};var q=b.parseJSON||function(n){return window["eval"]("("+n+")")};var k=function(Q,O,N){var M=Q.getResponseHeader("content-type")||"",n=O==="xml"||!O&&M.indexOf("xml")>=0,P=n?Q.responseXML:Q.responseText;if(n&&P.documentElement.nodeName==="parsererror"){b.error&&b.error("parsererror")}if(N&&N.dataFilter){P=N.dataFilter(P,O)}if(typeof P==="string"){if(O==="json"||!O&&M.indexOf("json")>=0){P=q(P)}else{if(O==="script"||!O&&M.indexOf("javascript")>=0){b.globalEval(P)}}}return P}}};b.fn.ajaxForm=function(c){if(this.length===0){var d={s:this.selector,c:this.context};if(!b.isReady&&d.s){a("DOM not ready, queuing ajaxForm");b(function(){b(d.s,d.c).ajaxForm(c)});return this}a("terminating; zero elements found by selector"+(b.isReady?"":" (DOM not ready)"));return this}return this.ajaxFormUnbind().bind("submit.form-plugin",function(f){if(!f.isDefaultPrevented()){f.preventDefault();b(this).ajaxSubmit(c)}}).bind("click.form-plugin",function(j){var i=j.target;var g=b(i);if(!(g.is(":submit,input:image"))){var f=g.closest(":submit");if(f.length==0){return}i=f[0]}var h=this;h.clk=i;if(i.type=="image"){if(j.offsetX!=undefined){h.clk_x=j.offsetX;h.clk_y=j.offsetY}else{if(typeof b.fn.offset=="function"){var k=g.offset();h.clk_x=j.pageX-k.left;h.clk_y=j.pageY-k.top}else{h.clk_x=j.pageX-i.offsetLeft;h.clk_y=j.pageY-i.offsetTop}}}setTimeout(function(){h.clk=h.clk_x=h.clk_y=null},100)})};b.fn.ajaxFormUnbind=function(){return this.unbind("submit.form-plugin click.form-plugin")};b.fn.formToArray=function(q){var p=[];if(this.length===0){return p}var d=this[0];var g=q?d.getElementsByTagName("*"):d.elements;if(!g){return p}var k,h,f,r,e,m,c;for(k=0,m=g.length;k ").get(0).files;z=void 0!==window.FormData;c.fn.ajaxSubmit=function(a){function d(b){b=c.param(b,a.traditional).replace(/\+/g," ").split("&");var f=b.length,h=[],d,e;for(d=0;d').val(e.extraData[n].value).appendTo(d)[0]):
+p.push(c('').val(e.extraData[n]).appendTo(d)[0]));e.iframeTarget||(r.appendTo("body"),q.attachEvent?q.attachEvent("onload",g):q.addEventListener("load",g,!1));setTimeout(a,15);d.submit.call?d.submit():document.createElement("form").submit.call(d)}finally{d.action=k,d.target=b,d.enctype=l,c(p).remove()}}function g(a){if(!k.aborted&&!C){try{t=q.contentWindow?q.contentWindow.document:q.contentDocument?q.contentDocument:q.document}catch(b){s("cannot access response document: ",
+b),a=x}if(a===y&&k)k.abort("timeout");else if(a==x&&k)k.abort("server abort");else if(t&&t.location.href!=e.iframeSrc||w){q.detachEvent?q.detachEvent("onload",g):q.removeEventListener("load",g,!1);a="success";var d;try{if(w)throw"timeout";var f="xml"==e.dataType||t.XMLDocument||c.isXMLDoc(t);s("isXml="+f);if(!f&&window.opera&&(null===t.body||!t.body.innerHTML)&&--E){s("requeing onLoad callback, DOM not available");setTimeout(g,250);return}var h=t.body?t.body:t.documentElement;k.responseText=h?h.innerHTML:
+null;k.responseXML=t.XMLDocument?t.XMLDocument:t;f&&(e.dataType="xml");k.getResponseHeader=function(a){return{"content-type":e.dataType}[a.toLowerCase()]};h&&(k.status=Number(h.getAttribute("status"))||k.status,k.statusText=h.getAttribute("statusText")||k.statusText);var m=(e.dataType||"").toLowerCase(),n=/(json|script|text)/.test(m);if(n||e.textarea){var p=t.getElementsByTagName("textarea")[0];if(p)k.responseText=p.value,k.status=Number(p.getAttribute("status"))||k.status,k.statusText=p.getAttribute("statusText")||
+k.statusText;else if(n){var u=t.getElementsByTagName("pre")[0],A=t.getElementsByTagName("body")[0];u?k.responseText=u.textContent?u.textContent:u.innerText:A&&(k.responseText=A.textContent?A.textContent:A.innerText)}}else"xml"==m&&!k.responseXML&&k.responseText&&(k.responseXML=F(k.responseText));try{z=G(k,m,e)}catch(D){a="parsererror",k.error=d=D||a}}catch(B){s("error caught: ",B),a="error",k.error=d=B||a}k.aborted&&(s("upload aborted"),a=null);k.status&&(a=200<=k.status&&300>k.status||304===k.status?
+"success":"error");"success"===a?(e.success&&e.success.call(e.context,z,"success",k),l&&c.event.trigger("ajaxSuccess",[k,e])):a&&(void 0===d&&(d=k.statusText),e.error&&e.error.call(e.context,k,a,d),l&&c.event.trigger("ajaxError",[k,e,d]));l&&c.event.trigger("ajaxComplete",[k,e]);l&&!--c.active&&c.event.trigger("ajaxStop");e.complete&&e.complete.call(e.context,k,a);C=!0;e.timeout&&clearTimeout(v);setTimeout(function(){e.iframeTarget||r.remove();k.responseXML=null},100)}}}var d=n[0],e,l,m,r,q,k,u,w,
+v;if(b)for(b=0;b '),r.css({position:"absolute",top:"-1000px",left:"-1000px"}));q=r[0];k={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(a){var b=
+"timeout"===a?"timeout":"aborted";s("aborting upload... "+b);this.aborted=1;try{q.contentWindow.document.execCommand&&q.contentWindow.document.execCommand("Stop")}catch(d){}q.src=e.iframeSrc;k.error=b;e.error&&e.error.call(e.context,k,b,a);l&&c.event.trigger("ajaxError",[k,e,b]);e.complete&&e.complete.call(e.context,k,b)}};(l=e.global)&&0===c.active++&&c.event.trigger("ajaxStart");l&&c.event.trigger("ajaxSend",[k,e]);if(e.beforeSend&&!1===e.beforeSend.call(e.context,k,e))e.global&&c.active--;else if(!k.aborted){(b=
+d.clk)&&(u=b.name)&&!b.disabled&&(e.extraData=e.extraData||{},e.extraData[u]=b.value,"image"==b.type&&(e.extraData[u+".x"]=d.clk_x,e.extraData[u+".y"]=d.clk_y));var y=1,x=2;b=c("meta[name=csrf-token]").attr("content");(u=c("meta[name=csrf-param]").attr("content"))&&b&&(e.extraData=e.extraData||{},e.extraData[u]=b);e.forceSync?f():setTimeout(f,10);var z,t,E=50,C,F=c.parseXML||function(a,b){window.ActiveXObject?(b=new ActiveXObject("Microsoft.XMLDOM"),b.async="false",b.loadXML(a)):b=(new DOMParser).parseFromString(a,
+"text/xml");return b&&b.documentElement&&"parsererror"!=b.documentElement.nodeName?b:null},H=c.parseJSON||function(a){return window.eval("("+a+")")},G=function(a,b,e){var d=a.getResponseHeader("content-type")||"",f="xml"===b||!b&&0<=d.indexOf("xml");a=f?a.responseXML:a.responseText;f&&"parsererror"===a.documentElement.nodeName&&c.error&&c.error("parsererror");e&&e.dataFilter&&(a=e.dataFilter(a,b));"string"===typeof a&&("json"===b||!b&&0<=d.indexOf("json")?a=H(a):("script"===b||!b&&0<=d.indexOf("javascript"))&&
+c.globalEval(a));return a}}}if(!this.length)return s("ajaxSubmit: skipping submit process - no element selected"),this;var h,b,n=this;a?"function"===typeof a&&(a={success:a}):a={};h=a.type||n[0].method;b=a.url||n[0].action;(b=(b="string"===typeof b?c.trim(b):"")||window.location.href||"")&&(b=(b.match(/^([^#]+)/)||[])[1]);a=c.extend(!0,{url:b,success:c.ajaxSettings.success,type:h||c.ajaxSettings.type,iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},a);b={};this.trigger("form-pre-serialize",
+[this,a,b]);if(b.veto)return s("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(a.beforeSerialize&&!1===a.beforeSerialize(this,a))return s("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var m=a.traditional;void 0===m&&(m=c.ajaxSettings.traditional);var p=[],l,r=this.formToArray(a.semantic,p);a.data&&(a.extraData=a.data,l=c.param(a.data,m));if(a.beforeSubmit&&!1===a.beforeSubmit(r,this,a))return s("ajaxSubmit: submit aborted via beforeSubmit callback"),this;this.trigger("form-submit-validate",
+[r,this,a,b]);if(b.veto)return s("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;b=c.param(r,m);l&&(b=b?b+"&"+l:l);"GET"==a.type.toUpperCase()?(a.url+=(0<=a.url.indexOf("?")?"&":"?")+b,a.data=null):a.data=b;var w=[];a.resetForm&&w.push(function(){n.resetForm()});a.clearForm&&w.push(function(){n.clearForm(a.includeHidden)});if(!a.dataType&&a.target){var v=a.success||function(){};w.push(function(b){var d=a.replaceTarget?"replaceWith":"html";c(a.target)[d](b).each(v,arguments)})}else a.success&&
+w.push(a.success);a.success=function(b,c,d){for(var f=a.context||this,e=0,g=w.length;eb)return null;for(var g=[],h=a.options,n=(f="select-one"==f)?b+1:h.length,b=f?b:0;b
+
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/README.txt b/sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/README.txt
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/README.txt
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/README.txt
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.info b/sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.info
similarity index 69%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.info
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.info
index b22843dd..0e60ee76 100644
--- a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.info
+++ b/sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.info
@@ -5,9 +5,9 @@ core = 7.x
configure = admin/config/media/imce_dir_man
dependencies[] = imce
-; Information added by drupal.org packaging script on 2012-10-30
-version = "7.x-1.1"
+; Information added by Drupal.org packaging script on 2014-03-23
+version = "7.x-1.2"
core = "7.x"
project = "imce_tools"
-datestamp = "1351598216"
+datestamp = "1395598458"
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.install b/sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.install
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.install
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.install
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.module b/sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.module
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_dir_man/imce_dir_man.module
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_dir_man/imce_dir_man.module
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/README.txt b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/README.txt
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/README.txt
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/README.txt
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.css b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.css
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.css
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.css
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.info b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.info
similarity index 66%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.info
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.info
index 4b81d279..c62bfc93 100644
--- a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.info
+++ b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.info
@@ -4,9 +4,9 @@ core = 7.x
package = Media
dependencies[] = imce
-; Information added by drupal.org packaging script on 2012-10-30
-version = "7.x-1.1"
+; Information added by Drupal.org packaging script on 2014-03-23
+version = "7.x-1.2"
core = "7.x"
project = "imce_tools"
-datestamp = "1351598216"
+datestamp = "1395598458"
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.install b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.install
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.install
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.install
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.js b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.js
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.js
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.js
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.module b/sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.module
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_file_path/imce_file_path.module
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_file_path/imce_file_path.module
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/README.txt b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/README.txt
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/README.txt
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/README.txt
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.css b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.css
similarity index 78%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.css
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.css
index 7bde8378..d8b9bf8a 100644
--- a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.css
+++ b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.css
@@ -11,3 +11,7 @@
background: url('loading.gif') no-repeat top left;
padding-left: 20px;
}
+
+#imce-search-results .link {
+ text-decoration: underline;
+}
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.info b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.info
similarity index 64%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.info
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.info
index 56113e80..0b90715b 100644
--- a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.info
+++ b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.info
@@ -4,9 +4,9 @@ core = 7.x
package = Media
dependencies[] = imce
-; Information added by drupal.org packaging script on 2012-10-30
-version = "7.x-1.1"
+; Information added by Drupal.org packaging script on 2014-03-23
+version = "7.x-1.2"
core = "7.x"
project = "imce_tools"
-datestamp = "1351598216"
+datestamp = "1395598458"
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.install b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.install
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.install
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.install
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.js b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.js
similarity index 91%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.js
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.js
index 0faacc0a..a3790cb0 100644
--- a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.js
+++ b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.js
@@ -16,12 +16,16 @@ imce.hooks.load.push(function() {
data = eval('(' + serverdata + ')');
var filelist = jQuery.map(data.files, function(fullpath, index) {
var li = document.createElement('li');
- /*jQuery(li).click(function () {
+ jQuery(li).click(function () {
file = fullpath.substr(fullpath.lastIndexOf('/') + 1);
dir = fullpath.substr(0, fullpath.lastIndexOf('/'));
+ if (dir!=imce.conf.dir) {
+ imce.navigate(dir);
+ imce.dirActivate(dir);
+ }
imce.dirActivate(dir);
imce.highlight(file);
- });*/
+ }).css('cursor','pointer').addClass('link');
if (index > 10) {
jQuery(li).addClass('toggle');
}
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.module b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.module
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/imce_search.module
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/imce_search.module
diff --git a/sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/loading.gif b/sites/all/modules/contrib/files/imce/imce_tools/imce_search/loading.gif
similarity index 100%
rename from sites/all/modules/contrib/files/imce/imce_tools/modules/imce_search/loading.gif
rename to sites/all/modules/contrib/files/imce/imce_tools/imce_search/loading.gif
diff --git a/sites/all/modules/contrib/form/honeypot/honeypot.admin.inc b/sites/all/modules/contrib/form/honeypot/honeypot.admin.inc
index cc0f3638..37bcba75 100644
--- a/sites/all/modules/contrib/form/honeypot/honeypot.admin.inc
+++ b/sites/all/modules/contrib/form/honeypot/honeypot.admin.inc
@@ -2,8 +2,7 @@
/**
* @file
- *
- * Honeypot administration form.
+ * Honeypot administration forms.
*/
/**
@@ -17,37 +16,37 @@ function honeypot_admin_form($form, &$form_state) {
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
- $form['configuration']['honeypot_protect_all_forms'] = array(
- '#type' => 'checkbox',
- '#title' => t('Protect all forms with Honeypot'),
- '#description' => t('Enable Honeypot protection for ALL forms on this site (it is best to only enable Honeypot for the forms you need below).'),
- '#default_value' => variable_get('honeypot_protect_all_forms', 0),
- );
- $form['configuration']['honeypot_protect_all_forms']['#description'] .= '
' . t('Page caching will be disabled on any page where a form is present if the Honeypot time limit is not set to 0.');
- $form['configuration']['honeypot_log'] = array(
- '#type' => 'checkbox',
- '#title' => t('Log blocked form submissions'),
- '#description' => t('Log submissions that are blocked due to Honeypot protection.'),
- '#default_value' => variable_get('honeypot_log', 0),
- );
- $form['configuration']['honeypot_element_name'] = array(
- '#type' => 'textfield',
- '#title' => t('Honeypot element name'),
- '#description' => t("The name of the Honeypot form field. It's usually most effective to use a generic name like email, homepage, or name, but this should be changed if it interferes with fields that are already in your forms. Must not contain spaces or special characters."),
- '#default_value' => variable_get('honeypot_element_name', 'url'),
- '#required' => TRUE,
- '#size' => 30,
- );
- $form['configuration']['honeypot_time_limit'] = array(
- '#type' => 'textfield',
- '#title' => t('Honeypot time limit'),
- '#description' => t('Minimum time required before form should be considered entered by a human instead of a bot. Set to 0 to disable.'),
- '#default_value' => variable_get('honeypot_time_limit', 5),
- '#required' => TRUE,
- '#size' => 5,
- '#field_suffix' => t('seconds'),
- );
- $form['configuration']['honeypot_time_limit']['#description'] .= '
' . t('Page caching will be disabled if there is a form protected by time limit on the page.');
+ $form['configuration']['honeypot_protect_all_forms'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Protect all forms with Honeypot'),
+ '#description' => t('Enable Honeypot protection for ALL forms on this site (it is best to only enable Honeypot for the forms you need below).'),
+ '#default_value' => variable_get('honeypot_protect_all_forms', 0),
+ );
+ $form['configuration']['honeypot_protect_all_forms']['#description'] .= '
' . t('Page caching will be disabled on any page where a form is present if the Honeypot time limit is not set to 0.');
+ $form['configuration']['honeypot_log'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Log blocked form submissions'),
+ '#description' => t('Log submissions that are blocked due to Honeypot protection.'),
+ '#default_value' => variable_get('honeypot_log', 0),
+ );
+ $form['configuration']['honeypot_element_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Honeypot element name'),
+ '#description' => t("The name of the Honeypot form field. It's usually most effective to use a generic name like email, homepage, or name, but this should be changed if it interferes with fields that are already in your forms. Must not contain spaces or special characters."),
+ '#default_value' => variable_get('honeypot_element_name', 'url'),
+ '#required' => TRUE,
+ '#size' => 30,
+ );
+ $form['configuration']['honeypot_time_limit'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Honeypot time limit'),
+ '#description' => t('Minimum time required before form should be considered entered by a human instead of a bot. Set to 0 to disable.'),
+ '#default_value' => variable_get('honeypot_time_limit', 5),
+ '#required' => TRUE,
+ '#size' => 5,
+ '#field_suffix' => t('seconds'),
+ );
+ $form['configuration']['honeypot_time_limit']['#description'] .= '
' . t('Page caching will be disabled if there is a form protected by time limit on the page.');
// Honeypot Enabled forms.
$form['enabled_forms'] = array(
@@ -64,84 +63,84 @@ function honeypot_admin_form($form, &$form_state) {
),
);
- // Generic forms.
- $form['enabled_forms']['general_forms'] = array('#markup' => '' . t('General Forms') . '
');
- // User register form.
- $form['enabled_forms']['honeypot_form_user_register_form'] = array(
+ // Generic forms.
+ $form['enabled_forms']['general_forms'] = array('#markup' => '' . t('General Forms') . '
');
+ // User register form.
+ $form['enabled_forms']['honeypot_form_user_register_form'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('User Registration form'),
+ '#default_value' => variable_get('honeypot_form_user_register_form', 0),
+ );
+ // User password form.
+ $form['enabled_forms']['honeypot_form_user_pass'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('User Password Reset form'),
+ '#default_value' => variable_get('honeypot_form_user_pass', 0),
+ );
+
+ // If webform.module enabled, add webforms.
+ if (module_exists('webform')) {
+ $form['enabled_forms']['honeypot_form_webforms'] = array(
'#type' => 'checkbox',
- '#title' => t('User Registration form'),
- '#default_value' => variable_get('honeypot_form_user_register_form', 0),
+ '#title' => t('Webforms (all)'),
+ '#default_value' => variable_get('honeypot_form_webforms', 0),
);
- // User password form.
- $form['enabled_forms']['honeypot_form_user_pass'] = array(
+ }
+
+ // If contact.module enabled, add contact forms.
+ if (module_exists('contact')) {
+ $form['enabled_forms']['contact_forms'] = array('#markup' => '' . t('Contact Forms') . '
');
+ // Sitewide contact form.
+ $form['enabled_forms']['honeypot_form_contact_site_form'] = array(
'#type' => 'checkbox',
- '#title' => t('User Password Reset form'),
- '#default_value' => variable_get('honeypot_form_user_pass', 0),
+ '#title' => t('Sitewide Contact form'),
+ '#default_value' => variable_get('honeypot_form_contact_site_form', 0),
);
+ // Sitewide personal form.
+ $form['enabled_forms']['honeypot_form_contact_personal_form'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Personal Contact forms'),
+ '#default_value' => variable_get('honeypot_form_contact_personal_form', 0),
+ );
+ }
- // If webform.module enabled, add webforms.
- if (module_exists('webform')) {
- $form['enabled_forms']['honeypot_form_webforms'] = array(
+ // If profile.module enabled, add profile forms.
+ if (module_exists('profile')) {
+ $form['enabled_forms']['profile_forms'] = array('#value' => '' . t('Profile Forms') . '
');
+ $form['enabled_forms']['honeypot_form_user_profile_form'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Profile forms (all)'),
+ '#default_value' => variable_get('honeypot_form_user_profile_form', 0),
+ );
+ }
+
+ // Get node types for node forms and node comment forms.
+ $types = node_type_get_types();
+ if (!empty($types)) {
+ // Node forms.
+ $form['enabled_forms']['node_forms'] = array('#markup' => '' . t('Node Forms') . '
');
+ foreach ($types as $type) {
+ $id = 'honeypot_form_' . $type->type . '_node_form';
+ $form['enabled_forms'][$id] = array(
'#type' => 'checkbox',
- '#title' => t('Webforms (all)'),
- '#default_value' => variable_get('honeypot_form_webforms', 0),
+ '#title' => t('@name node form', array('@name' => $type->name)),
+ '#default_value' => variable_get($id, 0),
);
}
- // If contact.module enabled, add contact forms.
- if (module_exists('contact')) {
- $form['enabled_forms']['contact_forms'] = array('#markup' => '' . t('Contact Forms') . '
');
- // Sitewide contact form.
- $form['enabled_forms']['honeypot_form_contact_site_form'] = array(
- '#type' => 'checkbox',
- '#title' => t('Sitewide Contact form'),
- '#default_value' => variable_get('honeypot_form_contact_site_form', 0),
- );
- // Sitewide personal form.
- $form['enabled_forms']['honeypot_form_contact_personal_form'] = array(
- '#type' => 'checkbox',
- '#title' => t('Personal Contact forms'),
- '#default_value' => variable_get('honeypot_form_contact_personal_form', 0),
- );
- }
-
- // If profile.module enabled, add profile forms.
- if (module_exists('profile')) {
- $form['enabled_forms']['profile_forms'] = array('#value' => '' . t('Profile Forms') . '
');
- $form['enabled_forms']['honeypot_form_user_profile_form'] = array(
- '#type' => 'checkbox',
- '#title' => t('Profile forms (all)'),
- '#default_value' => variable_get('honeypot_form_user_profile_form', 0),
- );
- }
-
- // Get node types for node forms and node comment forms.
- $types = node_type_get_types();
- if (!empty($types)) {
- // Node forms.
- $form['enabled_forms']['node_forms'] = array('#markup' => '' . t('Node Forms') . '
');
+ // Comment forms.
+ if (module_exists('comment')) {
+ $form['enabled_forms']['comment_forms'] = array('#markup' => '' . t('Comment Forms') . '
');
foreach ($types as $type) {
- $id = 'honeypot_form_' . $type->type . '_node_form';
+ $id = 'honeypot_form_comment_node_' . $type->type . '_form';
$form['enabled_forms'][$id] = array(
'#type' => 'checkbox',
- '#title' => t('@name node form', array('@name' => $type->name)),
+ '#title' => t('@name comment form', array('@name' => $type->name)),
'#default_value' => variable_get($id, 0),
);
}
-
- // Comment forms.
- if (module_exists('comment')) {
- $form['enabled_forms']['comment_forms'] = array('#markup' => '' . t('Comment Forms') . '
');
- foreach ($types as $type) {
- $id = 'honeypot_form_comment_node_' . $type->type . '_form';
- $form['enabled_forms'][$id] = array(
- '#type' => 'checkbox',
- '#title' => t('@name comment form', array('@name' => $type->name)),
- '#default_value' => variable_get($id, 0),
- );
- }
- }
}
+ }
// Add our own submit handler to clear honeypot's form cache on save.
$form['#submit'][] = 'honeypot_admin_form_submit';
diff --git a/sites/all/modules/contrib/form/honeypot/honeypot.api.php b/sites/all/modules/contrib/form/honeypot/honeypot.api.php
index a1d33b1a..dd9fdcd3 100644
--- a/sites/all/modules/contrib/form/honeypot/honeypot.api.php
+++ b/sites/all/modules/contrib/form/honeypot/honeypot.api.php
@@ -2,7 +2,6 @@
/**
* @file
- *
* API Functionality for Honeypot module.
*/
@@ -14,10 +13,10 @@
/**
* Alter the honeypot protections added to a particular form.
*
- * @param (array) $options
+ * @param array $options
* Protections that will be applied to the form. May be empty, or may include
* 'honeypot' and/or 'time_restriction'.
- * @param (array) $form
+ * @param array $form
* The Form API form to which protections will be added.
*/
function hook_honeypot_form_protections_alter(&$options, $form) {
@@ -34,10 +33,10 @@ function hook_honeypot_form_protections_alter(&$options, $form) {
* You can use this hook to track when and how many times certain protected
* forms are displayed to certain users, or for other tracking purposes.
*
- * @param (array) $options
+ * @param array $options
* Protections that were applied to the form. Includes 'honeypot' and/or
* 'time_restriction'.
- * @param (array) $form
+ * @param array $form
* The Form API form to which protections were added.
*/
function hook_honeypot_add_form_protection($options, $form) {
@@ -53,11 +52,11 @@ function hook_honeypot_add_form_protection($options, $form) {
* the user ID (0 if anonymous) of the user that was disallowed from submitting
* the form, and the reason (type) for the rejection of the form submission.
*
- * @param (string) $form_id
+ * @param string $form_id
* Form ID of the form the user was disallowed from submitting.
- * @param (int) $uid
+ * @param int $uid
* 0 for anonymous users, otherwise the user ID of the user.
- * @param (string) $type
+ * @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
@@ -76,15 +75,15 @@ function hook_honeypot_reject($form_id, $uid, $type) {
* hook to return additional time (in seconds) to honeypot when it is calculates
* the time limit for a particular form.
*
- * @param (int) $honeypot_time_limit
+ * @param int $honeypot_time_limit
* The current honeypot time limit (in seconds), to which any additions you
* return will be added.
- * @param (array) $form_values
+ * @param array $form_values
* Array of form values (may be empty).
- * @param (int) $number
+ * @param int $number
* Number of times the current user has already fallen into the honeypot trap.
*
- * @return (int) $additions
+ * @return int
* Additional time to add to the honeypot_time_limit, in seconds (integer).
*/
function hook_honeypot_time_limit($honeypot_time_limit, $form_values, $number) {
diff --git a/sites/all/modules/contrib/form/honeypot/honeypot.info b/sites/all/modules/contrib/form/honeypot/honeypot.info
index 7670d790..4bbd3742 100644
--- a/sites/all/modules/contrib/form/honeypot/honeypot.info
+++ b/sites/all/modules/contrib/form/honeypot/honeypot.info
@@ -6,9 +6,9 @@ package = "Spam control"
files[] = honeypot.test
-; Information added by drupal.org packaging script on 2013-09-13
-version = "7.x-1.15"
+; Information added by Drupal.org packaging script on 2014-05-30
+version = "7.x-1.17"
core = "7.x"
project = "honeypot"
-datestamp = "1379087801"
+datestamp = "1401478128"
diff --git a/sites/all/modules/contrib/form/honeypot/honeypot.install b/sites/all/modules/contrib/form/honeypot/honeypot.install
index 23c03996..e4eacf16 100644
--- a/sites/all/modules/contrib/form/honeypot/honeypot.install
+++ b/sites/all/modules/contrib/form/honeypot/honeypot.install
@@ -2,7 +2,6 @@
/**
* @file
- *
* Install, update and uninstall functions for the Honeypot module.
*/
@@ -38,8 +37,9 @@ function honeypot_schema() {
* Implements hook_install().
*/
function honeypot_install() {
- drupal_set_message(st("Honeypot installed successfully. Please !link to protect your forms from spam bots.", array(
- '!link' => l(st('configure Honeypot'), 'admin/config/content/honeypot')
+ $t = get_t();
+ drupal_set_message($t("Honeypot installed successfully. Please !link to protect your forms from spam bots.", array(
+ '!link' => l($t('configure Honeypot'), 'admin/config/content/honeypot'),
)));
}
@@ -65,7 +65,6 @@ function honeypot_update_7001() {
$ret = array();
// Leaving this in because I had it in version 1.3. Silly me.
-
return $ret;
}
diff --git a/sites/all/modules/contrib/form/honeypot/honeypot.module b/sites/all/modules/contrib/form/honeypot/honeypot.module
index 4f852c0d..22d3de1c 100644
--- a/sites/all/modules/contrib/form/honeypot/honeypot.module
+++ b/sites/all/modules/contrib/form/honeypot/honeypot.module
@@ -2,7 +2,6 @@
/**
* @file
- *
* Honeypot module, for deterring spam bots from completing Drupal forms.
*/
@@ -92,6 +91,43 @@ function honeypot_form_alter(&$form, &$form_state, $form_id) {
}
}
+/**
+ * Implements hook_trigger_info().
+ */
+function honeypot_trigger_info() {
+ return array(
+ 'honeypot' => array(
+ 'honeypot_reject' => array(
+ 'label' => t('Honeypot rejection'),
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_rules_event_info()
+ */
+function honeypot_rules_event_info() {
+ return array(
+ 'honeypot_reject' => array(
+ 'label' => t('Honeypot rejection'),
+ 'group' => t('Honeypot'),
+ 'variables' => array(
+ 'form_id' => array(
+ 'type' => 'text',
+ 'label' => t('Form ID of the form the user was disallowed from submitting.'),
+ ),
+ // Don't provide 'uid' in context because it is available as
+ // site:current-user:uid.
+ 'type' => array(
+ 'type' => 'text',
+ 'label' => t('String indicating the reason the submission was blocked.'),
+ ),
+ ),
+ ),
+ );
+}
+
/**
* Build an array of all the protected forms on the site, by form_id.
*
@@ -126,11 +162,11 @@ function honeypot_get_protected_forms() {
/**
* Form builder function to add different types of protection to forms.
*
- * @param $options (array)
+ * @param array $options
* Array of options to be added to form. Currently accepts 'honeypot' and
* 'time_restriction'.
*
- * @return $form_elements
+ * @return array
* Returns elements to be placed in a form's elements array to prevent spam.
*/
function honeypot_add_form_protection(&$form, &$form_state, $options = array()) {
@@ -160,9 +196,10 @@ function honeypot_add_form_protection(&$form, &$form_state, $options = array())
'#element_validate' => array('_honeypot_honeypot_validate'),
'#prefix' => '',
'#suffix' => '',
+ // Hide honeypot.
'#attached' => array(
'css' => array(
- '.' . $honeypot_class . ' { display: none !important; }' => array('type' => 'inline'), // Hide honeypot.
+ '.' . $honeypot_class . ' { display: none !important; }' => array('type' => 'inline'),
),
),
);
@@ -207,7 +244,12 @@ function _honeypot_honeypot_validate($element, &$form_state) {
/**
* Validate honeypot's time restriction field.
*/
-function _honeypot_time_restriction_validate($form, &$form_state) {
+function _honeypot_time_restriction_validate($element, &$form_state) {
+ // Don't do anything if the triggering element is a preview button.
+ if ($form_state['triggering_element']['#value'] == t('Preview')) {
+ return;
+ }
+
// Get the time value.
$honeypot_time = $form_state['values']['honeypot_time'];
@@ -218,7 +260,8 @@ function _honeypot_time_restriction_validate($form, &$form_state) {
// If not, throw an error.
if (time() < ($honeypot_time + $time_limit)) {
_honeypot_log($form_state['values']['form_id'], 'honeypot_time');
- $time_limit = honeypot_get_time_limit();
+ // Get the time limit again, since it increases after first failure.
+ $time_limit = honeypot_get_time_limit($form_state['values']);
$form_state['values']['honeypot_time'] = time();
form_set_error('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', array('@limit' => $time_limit)));
}
@@ -227,9 +270,9 @@ function _honeypot_time_restriction_validate($form, &$form_state) {
/**
* Log blocked form submissions.
*
- * @param $form_id
+ * @param string $form_id
* Form ID for the form on which submission was blocked.
- * @param $type
+ * @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
@@ -238,18 +281,17 @@ function _honeypot_log($form_id, $type) {
honeypot_log_failure($form_id, $type);
if (variable_get('honeypot_log', 0)) {
$variables = array(
- '%form' => $form_id,
- '@cause' => ($type == 'honeypot') ? t('submission of a value in the honeypot field') : t('submission of the form in less than minimum required time'),
+ '%form' => $form_id,
+ '@cause' => ($type == 'honeypot') ? t('submission of a value in the honeypot field') : t('submission of the form in less than minimum required time'),
);
watchdog('honeypot', 'Blocked submission of %form due to @cause.', $variables);
}
- return;
}
/**
* Look up the time limit for the current user.
*
- * @param $form_values
+ * @param array $form_values
* Array of form values (optional).
*/
function honeypot_get_time_limit($form_values = array()) {
@@ -271,7 +313,7 @@ function honeypot_get_time_limit($form_values = array()) {
))->fetchField();
}
// Don't add more than 30 days' worth of extra time.
- $honeypot_time_limit = $honeypot_time_limit + (int) min($honeypot_time_limit + exp($number), 2592000);
+ $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, 2592000);
$additions = module_invoke_all('honeypot_time_limit', $honeypot_time_limit, $form_values, $number);
if (count($additions)) {
$honeypot_time_limit += array_sum($additions);
@@ -283,9 +325,9 @@ function honeypot_get_time_limit($form_values = array()) {
/**
* Log the failed submision with timestamp.
*
- * @param $form_id
+ * @param string $form_id
* Form ID for the rejected form submission.
- * @param $type
+ * @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
@@ -309,4 +351,24 @@ function honeypot_log_failure($form_id, $type) {
// Allow other modules to react to honeypot rejections.
module_invoke_all('honeypot_reject', $form_id, $user->uid, $type);
+
+ // Trigger honeypot_reject action.
+ if (module_exists('trigger')) {
+ $aids = trigger_get_assigned_actions('honeypot_reject');
+ $context = array(
+ 'group' => 'honeypot',
+ 'hook' => 'honeypot_reject',
+ 'form_id' => $form_id,
+ // Do not provide $user in context because it is available as a global.
+ 'type' => $type,
+ );
+ // Honeypot does not act on any specific object.
+ $object = NULL;
+ actions_do(array_keys($aids), $object, $context);
+ }
+
+ // Trigger rules honeypot_reject event.
+ if (module_exists('rules')) {
+ rules_invoke_event('honeypot_reject', $form_id, $type);
+ }
}
diff --git a/sites/all/modules/contrib/form/honeypot/honeypot.test b/sites/all/modules/contrib/form/honeypot/honeypot.test
index 9dac9674..feccd0f5 100644
--- a/sites/all/modules/contrib/form/honeypot/honeypot.test
+++ b/sites/all/modules/contrib/form/honeypot/honeypot.test
@@ -9,8 +9,8 @@
* Test the functionality of the Honeypot module for an admin user.
*/
class HoneypotFormTestCase extends DrupalWebTestCase {
- protected $admin_user;
- protected $web_user;
+ protected $adminUser;
+ protected $webUser;
protected $node;
public static function getInfo() {
@@ -27,8 +27,10 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
// Set up required Honeypot variables.
variable_set('honeypot_element_name', 'url');
- variable_set('honeypot_time_limit', 0); // Disable time_limit protection.
- variable_set('honeypot_protect_all_forms', TRUE); // Test protecting all forms.
+ // Disable time_limit protection.
+ variable_set('honeypot_time_limit', 0);
+ // Test protecting all forms.
+ variable_set('honeypot_protect_all_forms', TRUE);
variable_set('honeypot_log', FALSE);
// Set up other required variables.
@@ -36,7 +38,7 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
variable_set('user_register', USER_REGISTER_VISITORS);
// Set up admin user.
- $this->admin_user = $this->drupalCreateUser(array(
+ $this->adminUser = $this->drupalCreateUser(array(
'administer honeypot',
'bypass honeypot protection',
'administer content types',
@@ -48,7 +50,7 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
));
// Set up web user.
- $this->web_user = $this->drupalCreateUser(array(
+ $this->webUser = $this->drupalCreateUser(array(
'access comments',
'post comments',
'create article content',
@@ -58,7 +60,7 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
$this->node = $this->drupalCreateNode(array(
'type' => 'article',
'promote' => 1,
- 'uid' => $this->web_user->uid,
+ 'uid' => $this->webUser->uid,
));
}
@@ -96,9 +98,12 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
$this->drupalPost('user/register', $edit, t('Create new account'));
// Form should have error message.
- $this->assertText(t('There was a problem with your form submission. Please wait'), 'Registration form protected by time limit.');
+ $this->assertText(t('There was a problem with your form submission. Please wait 6 seconds and try again.'), 'Registration form protected by time limit.');
}
+ /**
+ * Test comment form protection.
+ */
public function testProtectCommentFormNormal() {
$comment = 'Test comment.';
@@ -106,7 +111,7 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
variable_set('honeypot_time_limit', 0);
// Log in the web user.
- $this->drupalLogin($this->web_user);
+ $this->drupalLogin($this->webUser);
// Set up form and submit it.
$edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $comment;
@@ -118,7 +123,7 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
$comment = 'Test comment.';
// Log in the web user.
- $this->drupalLogin($this->web_user);
+ $this->drupalLogin($this->webUser);
// Set up form and submit it.
$edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $comment;
@@ -129,10 +134,96 @@ class HoneypotFormTestCase extends DrupalWebTestCase {
public function testProtectCommentFormHoneypotBypass() {
// Log in the admin user.
- $this->drupalLogin($this->admin_user);
+ $this->drupalLogin($this->adminUser);
// Get the comment reply form and ensure there's no 'url' field.
$this->drupalGet('comment/reply/' . $this->node->nid);
$this->assertNoText('id="edit-url" name="url"', 'Honeypot home page field not shown.');
}
+
+ /**
+ * Test node form protection.
+ */
+ public function testProtectNodeFormTooFast() {
+ // Log in the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Reset the time limit to 5 seconds.
+ variable_set('honeypot_time_limit', 5);
+
+ // Set up the form and submit it.
+ $edit["title"] = 'Test Page';
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+ $this->assertText(t('There was a problem with your form submission.'), 'Honeypot node form timestamp protection works.');
+ }
+
+ /**
+ * Test node form protection.
+ */
+ public function testProtectNodeFormPreviewPassthru() {
+ // Log in the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Post a node form using the 'Preview' button and make sure it's allowed.
+ $edit["title"] = 'Test Page';
+ $this->drupalPost('node/add/article', $edit, t('Preview'));
+ $this->assertNoText(t('There was a problem with your form submission.'), 'Honeypot not blocking node form previews.');
+ }
+}
+
+/**
+ * Test the functionality of the Honeypot module's integration with Trigger.
+ */
+class HoneypotTriggerTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Honeypot Trigger integration',
+ 'description' => 'Ensure that Honeypot triggers events correctly.',
+ 'group' => 'Form API',
+ );
+ }
+
+ public function setUp() {
+ // Enable modules required for this test.
+ parent::setUp(array('honeypot', 'trigger'));
+
+ // Set up required Honeypot variables.
+ variable_set('honeypot_element_name', 'url');
+ // Disable time_limit protection.
+ variable_set('honeypot_time_limit', 0);
+ // Test protecting all forms.
+ variable_set('honeypot_protect_all_forms', TRUE);
+ variable_set('honeypot_log', FALSE);
+
+ // Set up other required variables.
+ variable_set('user_email_verification', TRUE);
+ variable_set('user_register', USER_REGISTER_VISITORS);
+
+ // Assign new action to Honeypot form rejection Trigger.
+ db_insert('trigger_assignments')
+ ->fields(array(
+ 'hook' => 'honeypot_reject',
+ 'aid' => 'system_block_ip_action',
+ 'weight' => 1,
+ ))
+ ->execute();
+ }
+
+ /**
+ * Test trigger integration.
+ */
+ public function testHoneypotTriggerIntegration() {
+ // Set up form and submit it.
+ $edit['name'] = $this->randomName();
+ $edit['mail'] = $edit['name'] . '@example.com';
+ $edit['url'] = 'http://www.example.com/';
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+
+ // Make sure Honeypot is working.
+ $this->assertText(t('There was a problem with your form submission.'), 'Honeypot working correctly.');
+
+ // Visit the home page and make sure the user is banned.
+ $this->drupalGet('node');
+ $this->assertText(t('has been banned'), 'User banned successfully.');
+ }
}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/CHANGELOG.txt b/sites/all/modules/contrib/localisation/entity_translation/CHANGELOG.txt
index f09a453b..54cddea7 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/CHANGELOG.txt
+++ b/sites/all/modules/contrib/localisation/entity_translation/CHANGELOG.txt
@@ -3,6 +3,51 @@ Entity Translation 7.x-1.x, xxxx-xx-xx
--------------------------------------
+Entity Translation 7.x-1.0-beta4, 2015-01-23
+--------------------------------------------
+#2396103 by plach: Fixed [DATA LOSS] Translations deleted.
+#2218087 by plach: Fixed PHP Fatal error: Unsupported operand types in
+ modules/field_ui/field_ui.admin.inc on line 477.
+#2396653 by SchnWalter, mongolito404: Fix validation handler for the entity
+ language widget.
+#2395327 by damiankloip: FATAL error when requesting an edit path for a
+ non-existent entity.
+#2342787 by jcisio: Language widget submit handler runs on disabled entity
+ types.
+#2389945 by rafal.enden: Tests don't pass when using different source than
+ original node language.
+#2185523 by leon.nk: Document that entity_translation_enabled_bundle() does not
+ check whether the entity type is translatable.
+#2378195 by Johnny vd Laar: Node edit notice when author of a translation is
+ deleted.
+#2382713 by seanr: entity_translation_update_7006 fails because of SQL error
+ before variable_set('entity_translation_revision_enabled', FALSE); can run.
+#1046282 by plach, good_man, miro_dietiker, Gábor Hojtsy: Make the module work
+ with revisions.
+#2179183 by dkingofpa, Jānis Bebrītis | SocialNicheGuru: Fixed Fatal error:
+ Unsupported operand types in /entity_translation.module on line 188.
+#1605406 by joelrosen, Jose Reyero, paulihuhtiniemi, guillaumev | mindaugasd:
+ Exposed translated field content to Views so that multiple language
+ translations can be shown simultaneously.
+#2160559 by w3wfr: Warning because of empty value line 675.
+#2151503 by SebCorbin: Check for translation access for Add links.
+#2130091 by peximo: Media edit dialog integration bug.
+#2065221 by catch, plach: Fixed Static cache for handlers should key by $vid.
+#1516202 by fabsor, spotzero: Added a CTools access plugin for checking if a
+ translation is available.
+#2027513 by GaëlG, jcisio: Added support for nested values for 'edit form' in
+ hook_entity_info().
+#2073231 by plach: Rename entity_translation_form_language() to
+ entity_translation_get_existing_language().
+#2072865 by douggreen: Fixed Only replace 'delete' button with
+ 'delete translation', don't add one that doesn't exist.
+#1865244 by plach | bojanz, agoradesign, ciss, interdruper, joel_osc: Added
+ support for multiple translation handlers on the same form.
+#2055491 by plach: Fixed Synced image fields cannot be emptied.
+#1866076 by grndlvl: Added support for non-entity load tokens when declaring
+ translations.
+
+
Entity Translation 7.x-1.0-beta3, 2013-07-23
--------------------------------------------
#2037789 by Jelle_S: Fixed Default to user_access() access callback like drupal
@@ -33,7 +78,7 @@ Entity Translation 7.x-1.0-beta3, 2013-07-23
#1947764 by plach: Improve bundle translatability checks.
#1370900 by mojzis, plach | mgladding: Fixed Fatal Error: Cannot access empty
property in translation.handler.inc.
-Issue #1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
+#1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
content-translation enabled bundles.
#1933742 by das-peter: Fixed Delete translation doesn't flush the entity cache.
#1903024 by fabsor | dimitrileonidas: Fixed SQL error in Views when adding an
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.admin.inc b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.admin.inc
index 9b60df6e..380560d2 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.admin.inc
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.admin.inc
@@ -204,6 +204,8 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
}
$handler = entity_translation_get_handler($entity_type, $entity);
+ // Ensure $entity holds an entity object and not an id.
+ $entity = $handler->getEntity();
$handler->initPathScheme();
// Initialize translations if they are empty.
@@ -284,7 +286,7 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
// No such translation in the set yet: help user to create it.
$row_title = $source_name = t('n/a');
- if ($source != $langcode && $handler->getAccess('update')) {
+ if ($source != $langcode && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$translatable = FALSE;
@@ -382,12 +384,14 @@ function theme_entity_translation_overview_outdated($variables){
*/
function entity_translation_delete_confirm($form, $form_state, $entity_type, $entity, $langcode) {
$handler = entity_translation_get_handler($entity_type, $entity);
+ $handler->setFormLanguage($langcode);
$languages = language_list();
$form = array(
'#handler' => $handler,
'#entity_type' => $entity_type,
- '#entity' => $entity,
+ // Ensure $entity holds an entity object and not an id.
+ '#entity' => $handler->getEntity(),
'#language' => $langcode,
);
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.api.php b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.api.php
index 33b447f6..a336afd5 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.api.php
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.api.php
@@ -64,7 +64,7 @@
* - skip original values access: A flag specifying whether skipping access
* control when editing original values for this entity. Defaults to FALSE.
* - bundle callback: A callback to check whether the passed bundle has entity
- * translation enabled. If empty all bundles are supposed to be enabled.
+ * translation enabled. Defaults to TRUE for all bundles.
* - default settings: The defaults to be applied to settings when an explicit
* choice is missing.
*/
@@ -151,3 +151,16 @@ function hook_entity_translation_update($entity_type, $entity, $translation, $va
*/
function hook_entity_translation_delete($entity_type, $entity, $langcode) {
}
+
+/**
+ * Allows modules to act when a revision translation is deleted.
+ *
+ * @param $entity_type
+ * The entity type.
+ * @param $entity
+ * The entity.
+ * @param $langcode
+ * The langcode of the revision translation which was deleted.
+ */
+function hook_entity_translation_delete_revision($entity_type, $entity, $langcode) {
+}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.info b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.info
index f8cb4dd9..c020cc0a 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.info
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.info
@@ -5,6 +5,7 @@ core = 7.x
configure = admin/config/regional/entity_translation
dependencies[] = locale (>7.14)
+files[] = includes/translation.handler_factory.inc
files[] = includes/translation.handler.inc
files[] = includes/translation.handler.comment.inc
files[] = includes/translation.handler.node.inc
@@ -19,10 +20,11 @@ files[] = views/entity_translation_handler_field_label.inc
files[] = views/entity_translation_handler_filter_entity_type.inc
files[] = views/entity_translation_handler_filter_language.inc
files[] = views/entity_translation_handler_filter_translation_exists.inc
+files[] = views/entity_translation_handler_field_field.inc
-; Information added by drupal.org packaging script on 2013-07-23
-version = "7.x-1.0-beta3"
+; Information added by Drupal.org packaging script on 2015-01-22
+version = "7.x-1.0-beta4"
core = "7.x"
project = "entity_translation"
-datestamp = "1374601567"
+datestamp = "1421971088"
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.install b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.install
index f93aa303..0253e842 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.install
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.install
@@ -9,6 +9,8 @@
* Implements hook_schema().
*/
function entity_translation_schema() {
+ $schema = array();
+
$schema['entity_translation'] = array(
'description' => 'Table to track entity translations',
'fields' => array(
@@ -25,7 +27,12 @@ function entity_translation_schema() {
'not null' => TRUE,
'description' => 'The entity id this translation relates to',
),
- // @todo: Consider an integer field for 'language'.
+ 'revision_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity revision id this translation relates to',
+ ),
'language' => array(
'type' => 'varchar',
'length' => 32,
@@ -73,6 +80,78 @@ function entity_translation_schema() {
),
'primary key' => array('entity_type', 'entity_id', 'language'),
);
+
+ $schema['entity_translation_revision'] = array(
+ 'description' => 'Table to track entity translation revisions',
+ 'fields' => array(
+ 'entity_type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The entity type this translation revision relates to',
+ ),
+ 'entity_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity id this translation revision relates to',
+ ),
+ 'revision_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity revision id this translation relates to',
+ ),
+ 'language' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The target language for this translation revision.',
+ ),
+ 'source' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The source language from which this translation revision was created.',
+ ),
+ 'uid' => array(
+ 'description' => 'The author of this translation revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'Boolean indicating whether the translation revision is published (visible to non-administrators).',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'translate' => array(
+ 'description' => 'A boolean indicating whether this translation revision needs to be updated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the translation revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the translation revision was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('entity_type', 'revision_id', 'language'),
+ 'indexes'=> array('revision_id' => array('revision_id')),
+ );
+
return $schema;
}
@@ -93,6 +172,9 @@ function entity_translation_install() {
// Make translation use the content language type.
variable_set('translation_language_type', LANGUAGE_TYPE_CONTENT);
+
+ // Enable revision support for entity translation.
+ variable_set('entity_translation_revision_enabled', TRUE);
}
/**
@@ -254,3 +336,113 @@ function entity_translation_update_7003() {
function entity_translation_update_7004() {
entity_info_cache_clear();
}
+
+/**
+ * Rebuild the class registry to pick up the translation handler factory class.
+ */
+function entity_translation_update_7005() {
+ registry_rebuild();
+}
+
+/**
+ * Add revision schema for entity translation metadata.
+ */
+function entity_translation_update_7006() {
+ // Create revision id column.
+ $spec = array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ // If we have existing data we cannot enforce this to be NOT NULL.
+ 'not null' => FALSE,
+ 'description' => 'The entity revision id this translation relates to',
+ );
+ db_add_field('entity_translation', 'revision_id', $spec);
+
+ // Create the entity translation revision schema.
+ $table = array(
+ 'description' => 'Table to track entity translation revisions',
+ 'fields' => array(
+ 'entity_type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The entity type this translation revision relates to',
+ ),
+ 'entity_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity id this translation revision relates to',
+ ),
+ 'revision_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity revision id this translation relates to',
+ ),
+ 'language' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The target language for this translation revision.',
+ ),
+ 'source' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The source language from which this translation revision was created.',
+ ),
+ 'uid' => array(
+ 'description' => 'The author of this translation revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'Boolean indicating whether the translation revision is published (visible to non-administrators).',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'translate' => array(
+ 'description' => 'A boolean indicating whether this translation revision needs to be updated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the translation revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the translation revision was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('entity_type', 'revision_id', 'language'),
+ 'indexes'=> array('revision_id' => array('revision_id')),
+ );
+ db_create_table('entity_translation_revision', $table);
+}
+
+/**
+ * Disable revision support on existing installations.
+ */
+function entity_translation_update_7007() {
+ // Revision support is not enabled by default on existing installations as
+ // making it work implies copying translation metadata to the
+ // {entity_translation_revision} table for all the existing translations.
+ // Since this process cannot be reliably implemented in an update function,
+ // we leave the choice of manually performing the upgrade to people with the
+ // required skills to do so. Be aware that enabling revision support on sites
+ // where data has not been manually migrated may cause translation metadata to
+ // be permanently lost or corrupted. See https://www.drupal.org/node/2396103.
+ variable_set('entity_translation_revision_enabled', FALSE);
+}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.module b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.module
index b9417d6b..236d4880 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation.module
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation.module
@@ -129,7 +129,7 @@ function entity_translation_entity_info() {
* Processes the given path schemes and fill-in default values.
*/
function _entity_translation_process_path_schemes($entity_type, &$et_info) {
- $path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme'));
+ $path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme', 'edit tabs'));
// Insert the default path scheme into the 'path schemes' array and remove
// respective elements from the entity_translation info array.
@@ -164,6 +164,7 @@ function _entity_translation_process_path_schemes($entity_type, &$et_info) {
$et_info['path schemes'][$delta] += array(
'admin theme' => TRUE,
'path wildcard' => "%$entity_type",
+ 'edit tabs' => TRUE,
);
}
}
@@ -184,6 +185,9 @@ function entity_translation_entity_info_alter(&$entity_info) {
// not. As a matter of fact we might need them to correctly switch field
// translatability when a field is shared across different entity types.
$et_info += array('class' => 'EntityTranslationDefaultHandler');
+ if (!isset($entity_info[$entity_type]['entity keys'])) {
+ $entity_info[$entity_type]['entity keys'] = array();
+ }
$entity_info[$entity_type]['entity keys'] += array('translations' => 'translations');
if (entity_translation_enabled($entity_type, NULL, TRUE)) {
@@ -452,21 +456,23 @@ function entity_translation_menu_alter(&$items) {
$edit_item['access arguments'] = array_merge($args, $original_item['access arguments']);
// Edit translation callback.
- $translation_position = count($edit_path_parts);
- $args = array($entity_type, $entity_position, $translation_position, $original_item);
- $items["$edit_path/%entity_translation_language"] = array(
- 'type' => MENU_DEFAULT_LOCAL_TASK,
- 'title callback' => 'entity_translation_edit_title',
- 'title arguments' => array($translation_position),
- 'page callback' => 'entity_translation_edit_page',
- 'page arguments' => array_merge($args, $original_item['page arguments']),
- 'access callback' => 'entity_translation_edit_access',
- 'access arguments' => array_merge($args, $original_item['access arguments']),
- )
- // We need to inherit the remaining menu item keys, mostly 'module'
- // and 'file' to keep ajax callbacks working (see form_get_cache() and
- // drupal_retrieve_form()).
- + $original_item;
+ if ($scheme['edit tabs'] !== FALSE) {
+ $translation_position = count($edit_path_parts);
+ $args = array($entity_type, $entity_position, $translation_position, $original_item);
+ $items["$edit_path/%entity_translation_language"] = array(
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'title callback' => 'entity_translation_edit_title',
+ 'title arguments' => array($translation_position),
+ 'page callback' => 'entity_translation_edit_page',
+ 'page arguments' => array_merge($args, $original_item['page arguments']),
+ 'access callback' => 'entity_translation_edit_access',
+ 'access arguments' => array_merge($args, $original_item['access arguments']),
+ )
+ // We need to inherit the remaining menu item keys, mostly 'module'
+ // and 'file' to keep ajax callbacks working (see form_get_cache() and
+ // drupal_retrieve_form()).
+ + $original_item;
+ }
// Add translation callback.
$add_path = "$edit_path/add/%entity_translation_language/%entity_translation_language";
@@ -533,7 +539,7 @@ function entity_translation_edit_page() {
// Set the current form language.
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->initPathScheme();
- $langcode = entity_translation_form_language($langcode, $handler);
+ $langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
$handler->setFormLanguage($langcode);
// Display the entity edit form.
@@ -548,10 +554,20 @@ function entity_translation_edit_access() {
$entity_type = array_shift($args);
$entity = array_shift($args);
$langcode = array_shift($args);
+ $edit_form_item = array_shift($args);
+ $access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
$handler = entity_translation_get_handler($entity_type, $entity);
+
+ // First, check a handler has been loaded. This could be empty if a
+ // non-existent entity edit path has been requested, for example. Delegate
+ // directly to the edit form item access callback in this case.
+ if (empty($handler)) {
+ return _entity_translation_callback($access_callback, $args, $edit_form_item);
+ }
+
$translations = $handler->getTranslations();
- $langcode = entity_translation_form_language($langcode, $handler);
+ $langcode = $langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
// The user must be explicitly allowed to access the original values if
// workflow permissions are enabled.
@@ -567,8 +583,6 @@ function entity_translation_edit_access() {
// is language neutral we need to let editors access it.
$enabled_languages = entity_translation_languages($entity_type, $entity);
if (isset($enabled_languages[$langcode]) || $langcode == LANGUAGE_NONE) {
- $edit_form_item = array_shift($args);
- $access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
}
@@ -579,11 +593,6 @@ function entity_translation_edit_access() {
/**
* Determines the current form language.
*
- * Based on the requested language and the translations available for the entity
- * being edited, determines the active form language. This takes into account
- * language fallback rules so that the translation being edited matches the one
- * being viewed.
- *
* @param $langcode
* The requested language code.
* @param EntityTranslationHandlerInterface $handler
@@ -591,8 +600,35 @@ function entity_translation_edit_access() {
*
* @return
* A valid language code.
+ *
+ * @deprecated This is no longer used and will be removed in the first stable
+ * release.
*/
function entity_translation_form_language($langcode, $handler) {
+ return entity_translation_get_existing_language($handler->getEntity(), $handler->getEntityType(), $langcode);
+}
+
+/**
+ * Determines an existing translation language.
+ *
+ * Based on the requested language and the translations available for the given
+ * entity, determines an existing translation language. This takes into account
+ * language fallback rules.
+ *
+ * @param $entity_type
+ * The type of the entity.
+ * @param $entity
+ * The entity whose existing translation language has to be returned.
+ * @param $langcode
+ * (optional) The requested language code. Defaults to the current content
+ * language.
+ *
+ * @return
+ * A valid language code.
+ */
+function entity_translation_get_existing_language($entity_type, $entity, $langcode = NULL) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
+
if (empty($langcode)) {
$langcode = $GLOBALS['language_content']->language;
}
@@ -671,7 +707,7 @@ function _entity_translation_callback($callback, $args, $info = array()) {
function entity_translation_admin_paths() {
$paths = array();
foreach (entity_get_info() as $entity_type => $info) {
- if (entity_translation_enabled($entity_type, NULL, TRUE)) {
+ if (isset($info['translation']['entity_translation']['path schemes']) && entity_translation_enabled($entity_type, NULL, TRUE)) {
foreach ($info['translation']['entity_translation']['path schemes'] as $scheme) {
if (!empty($scheme['admin theme'])) {
if (isset($scheme['translate path'])) {
@@ -695,8 +731,12 @@ function entity_translation_admin_paths() {
*/
function entity_translation_tab_access($entity_type, $entity) {
if (drupal_multilingual() && (user_access('translate any entity') || user_access("translate $entity_type entities"))) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
+ // Ensure $entity holds an entity object and not an id.
+ $entity = $handler->getEntity();
+
$enabled = entity_translation_enabled($entity_type, $entity);
- return $enabled && entity_translation_get_handler($entity_type, $entity)->getLanguage() != LANGUAGE_NONE;
+ return $enabled && $handler->getLanguage() != LANGUAGE_NONE;
}
return FALSE;
}
@@ -831,6 +871,13 @@ function entity_translation_field_extra_fields() {
if (entity_translation_enabled($entity_type)) {
$bundles = !empty($info[$entity_type]['bundles']) ? array_keys($info[$entity_type]['bundles']) : array($entity_type);
foreach ($bundles as $bundle) {
+ // @todo Clean this up in https://www.drupal.org/node/1661348.
+ if ($entity_type == 'taxonomy_term') {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($bundle);
+ if ($vocabulary && module_invoke('i18n_taxonomy', 'vocabulary_mode', $vocabulary, 4)) {
+ continue;
+ }
+ }
$settings = entity_translation_settings($entity_type, $bundle);
if (empty($settings['hide_language_selector']) && entity_translation_enabled_bundle($entity_type, $bundle) && ($handler = entity_translation_get_handler($entity_type, $bundle))) {
$language_key = $handler->getLanguageKey();
@@ -1060,6 +1107,10 @@ function entity_translation_sync($entity_type, $entity) {
// If an item has been removed we do not store its translations.
if ($removed) {
+ // Ensure items are actually removed.
+ if (!isset($entity->{$field_name}[$langcode])) {
+ $entity->{$field_name}[$langcode] = array();
+ }
continue;
}
// If a synchronized column has changed we need to override the full
@@ -1120,12 +1171,25 @@ function entity_translation_field_attach_delete($entity_type, $entity) {
}
}
+/**
+ * Implements hook_field_attach_delete_revision().
+ */
+function entity_translation_field_attach_delete_revision($entity_type, $entity) {
+ if (entity_translation_enabled($entity_type, $entity)) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
+ $handler->removeRevisionTranslations();
+ $handler->saveTranslations();
+ }
+}
+
/**
* Implementation of hook_field_attach_form().
*/
function entity_translation_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
- // Skip recursing into the source form.
- if (empty($form['#entity_translation_source_form']) && ($handler = entity_translation_entity_form_get_handler($form, $form_state))) {
+ // Avoid recursing into the source form.
+ list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
+ if (empty($form['#entity_translation_source_form']) && entity_translation_enabled($entity_type, $bundle)) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
$langcode = !empty($langcode) ? $langcode : $handler->getLanguage();
$form_langcode = $handler->getFormLanguage();
$translations = $handler->getTranslations();
@@ -1142,8 +1206,6 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
// with the correct form language and replace the field elements with the
// correct ones.
if ($update_langcode || ($source && !isset($translations->data[$form_langcode]) && isset($translations->data[$source]) && empty($form_state['rebuild']))) {
- list(, , $bundle) = entity_extract_ids($entity_type, $entity);
-
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
@@ -1153,25 +1215,30 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
// user can find the form items already populated with the source values
// while the field form element holds the correct language information.
if ($field['translatable']) {
- $form[$field_name]['#field_name'] = $field_name;
- $form[$field_name]['#source'] = $update_langcode ? $form_langcode : $source;
- $form[$field_name]['#previous'] = NULL;
+ $element = &$form[$field_name];
+ $element['#entity_type'] = $entity_type;
+ $element['#entity'] = $entity;
+ $element['#entity_id'] = $id;
+ $element['#field_name'] = $field_name;
+ $element['#source'] = $update_langcode ? $form_langcode : $source;
+ $element['#previous'] = NULL;
+ $element['#form_parents'] = $form['#parents'];
// If we are updating the form language we need to make sure that the
// wrong language is unset and the right one is stored in the field
// element (see entity_translation_prepare_element()).
if ($update_langcode) {
- $form[$field_name]['#previous'] = $form[$field_name]['#language'];
- $form[$field_name]['#language'] = $form_langcode;
+ $element['#previous'] = $element['#language'];
+ $element['#language'] = $form_langcode;
}
// Swap default values during form processing to avoid recursion. We
// try to act before any other callback so that the correct values are
// already in place for them.
- if (!isset($form[$field_name]['#process'])) {
- $form[$field_name]['#process'] = array();
+ if (!isset($element['#process'])) {
+ $element['#process'] = array();
}
- array_unshift($form[$field_name]['#process'], 'entity_translation_prepare_element');
+ array_unshift($element['#process'], 'entity_translation_prepare_element');
}
}
}
@@ -1205,18 +1272,31 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
* Form element process callback.
*/
function entity_translation_prepare_element($element, &$form_state) {
- $source_form = &drupal_static(__FUNCTION__, array());
+ static $drupal_static_fast;
+ if (!isset($drupal_static_fast)) {
+ $drupal_static_fast['source_forms'] = &drupal_static(__FUNCTION__, array());
+ }
+
+ $source_forms = &$drupal_static_fast['source_forms'];
$form = $form_state['complete form'];
$build_id = $form['#build_id'];
$source = $element['#source'];
+ $entity_type = $element['#entity_type'];
+ $id = $element['#entity_id'];
- if (!isset($source_form[$build_id][$source])) {
- $source_form[$build_id][$source] = array('#entity_translation_source_form' => TRUE);
+ // Key the source form cache per entity type and entity id to allow for
+ // multiple entities on the same entity form.
+ if (!isset($source_forms[$build_id][$source][$entity_type][$id])) {
+ $source_form = array(
+ '#entity_translation_source_form' => TRUE,
+ '#parents' => $element['#form_parents'],
+ );
$source_form_state = $form_state;
- $info = entity_translation_edit_form_info($form, $form_state);
- field_attach_form($info['entity type'], $info['entity'], $source_form[$build_id][$source], $source_form_state, $source);
+ field_attach_form($entity_type, $element['#entity'], $source_form, $source_form_state, $source);
+ $source_forms[$build_id][$source][$entity_type][$id] = &$source_form;
}
+ $source_form = &$source_forms[$build_id][$source][$entity_type][$id];
$langcode = $element['#language'];
$field_name = $element['#field_name'];
@@ -1224,8 +1304,8 @@ function entity_translation_prepare_element($element, &$form_state) {
// language information from source to target language, this way the user can
// find the form items already populated with the source values while the
// field form element holds the correct language information.
- if (isset($source_form[$build_id][$source][$field_name][$source])) {
- $element[$langcode] = $source_form[$build_id][$source][$field_name][$source];
+ if (isset($source_form[$field_name][$source])) {
+ $element[$langcode] = $source_form[$field_name][$source];
entity_translation_form_element_language_replace($element, $source, $langcode);
unset($element[$element['#previous']]);
}
@@ -1245,7 +1325,7 @@ function entity_translation_form_element_language_replace(&$element, $source, $l
// Replace specific occurrences of the source language with the target
// language.
foreach (element_properties($element) as $key) {
- if ($key === '#language') {
+ if ($key === '#language' && $element[$key] != LANGUAGE_NONE) {
$element[$key] = $langcode;
}
elseif ($key === '#parents' || $key === '#field_parents') {
@@ -1362,30 +1442,32 @@ function _entity_translation_element_title_append(&$element, $suffix) {
* Implements hook_form_alter().
*/
function entity_translation_form_alter(&$form, &$form_state) {
- if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) {
- if (!$handler->isNewEntity()) {
- $handler->entityForm($form, $form_state);
- $translations = $handler->getTranslations();
- $form_langcode = $handler->getFormLanguage();
- if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
- // Hide shared form elements if the user is not allowed to edit them.
- $handler->entityFormSharedElements($form);
+ if ($info = entity_translation_edit_form_info($form, $form_state)) {
+ $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
+ if (entity_translation_enabled($info['entity type'], $info['entity'])) {
+ if (!$handler->isNewEntity()) {
+ $handler->entityForm($form, $form_state);
+ $translations = $handler->getTranslations();
+ $form_langcode = $handler->getFormLanguage();
+ if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
+ // Hide shared form elements if the user is not allowed to edit them.
+ $handler->entityFormSharedElements($form);
+ }
}
+ else {
+ $handler->entityFormLanguageWidget($form, $form_state);
+ }
+ // We need to process the posted form as early as possible to update the
+ // form language value.
+ array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
}
+ // We might have an entity form for an entity or a bundle not enabled for
+ // translation. In this case we might need to deal with entity and field
+ // languages anyway, since fields may be shared among different bundles and
+ // entity types.
else {
$handler->entityFormLanguageWidget($form, $form_state);
}
- // We need to process the posted form as early as possible to update the
- // form language value.
- array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
- }
- // We might have an entity form for an entity or a bundle not enabled for
- // translation. In this case we might need to deal with entity and field
- // languages anyway, since fields may be shared among different bundles and
- // entity types.
- elseif ($info = entity_translation_edit_form_info($form, $form_state)) {
- $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
- $handler->entityFormLanguageWidget($form, $form_state);
}
}
@@ -1431,16 +1513,22 @@ function entity_translation_entity_form_validate($form, &$form_state) {
}
/**
- * Submit handler for the entity language widget.
+ * Validation handler for the entity language widget.
*/
-function entity_translation_language_widget_submit($form, &$form_state) {
+function entity_translation_entity_form_language_update($element, &$form_state, $form) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
- // On non-translatable entities, we need to handle just the entity and field
- // language.
- if (empty($handler) && ($info = entity_translation_edit_form_info($form, $form_state))) {
- $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
+ // Ensure the handler form language match the actual one. This is mainly
+ // needed when responding to an AJAX request where the languages cannot be set
+ // from the usual page callback.
+ if (!empty($form_state['entity_translation']['form_langcode'])) {
+ $handler->setFormLanguage($form_state['entity_translation']['form_langcode']);
+ }
+ // When responding to an AJAX request we should ignore any change in the
+ // language widget as it may alter the field language expected by the AJAX
+ // callback.
+ if (empty($form_state['triggering_element']['#ajax'])) {
+ $handler->entityFormLanguageWidgetSubmit($form, $form_state);
}
- $handler->entityFormLanguageWidgetSubmit($form, $form_state);
}
/**
@@ -1462,7 +1550,7 @@ function entity_translation_entity_form_submit($form, &$form_state) {
* Mark translations as outdated if the submitted value is true.
*/
function entity_translation_field_attach_submit($entity_type, $entity, $form, &$form_state) {
- if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) {
+ if (($handler = entity_translation_entity_form_get_handler($form, $form_state)) && entity_translation_enabled($entity_type, $entity)) {
// Update the wrapped entity with the submitted values.
$handler->setEntity($entity);
$handler->entityFormSubmit($form, $form_state);
@@ -1648,6 +1736,9 @@ function entity_translation_enabled($entity_type, $entity = NULL, $skip_handler
/**
* Determines whether the given entity bundle is translatable.
*
+ * NOTE: Does not check for whether the entity type is translatable.
+ * Consider using entity_translation_enabled() instead.
+ *
* @param $entity_type
* The entity type the bundle to be checked belongs to.
* @param $bundle
@@ -1707,32 +1798,7 @@ function entity_translation_settings($entity_type, $bundle) {
* A valid language code.
*/
function entity_translation_language($entity_type, $entity) {
- // If a form has been post, we need to check its state to verify if any form
- // translation handler is stored there. This is mainly needed when responding
- // to an AJAX request where the form language cannot be set from the page
- // callback.
- $handler = entity_translation_current_form_get_handler();
-
- // Make sure we always have a translation handler instance available.
- if (empty($handler)) {
- $handler = entity_translation_get_handler($entity_type, $entity);
- }
- // If a translation handler associated to the current form is found, we need
- // to update the wrapped entity. This way submitted values will be picked up.
- // Other entities may be created or saved while submitting the current one,
- // hence we need to check we are dealing with it.
- elseif ($handler->isWrappedEntity($entity_type, $entity)) {
- $langcode = $handler->getLanguage();
- $handler->setEntity($entity);
- $submitted_langcode = $handler->getLanguage();
- // If the entity language has changed we are editing the original values. In
- // this case we need to update the current form language with the submitted
- // one.
- if ($submitted_langcode != $langcode) {
- $handler->setFormLanguage($submitted_langcode);
- }
- }
-
+ $handler = entity_translation_get_handler($entity_type, $entity);
$langcode = $handler->getFormLanguage();
return !empty($langcode) ? $langcode : $handler->getLanguage();
}
@@ -1745,61 +1811,20 @@ function entity_translation_language($entity_type, $entity) {
* @param $entity
* (optional) The entity to be translated. A bundle name may be passed to
* instantiate an empty entity.
- * @param $update
- * (optional) Instances are statically cached: if this is TRUE the wrapped
- * entity will be replaced by the passed one.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
-function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $update = FALSE) {
- static $drupal_static_fast;
- if (!isset($drupal_static_fast['handlers'])) {
- $drupal_static_fast['handlers'] = &drupal_static(__FUNCTION__, array());
+function entity_translation_get_handler($entity_type = NULL, $entity = NULL) {
+ if (class_exists('EntityTranslationHandlerFactory')) {
+ $factory = EntityTranslationHandlerFactory::getInstance();
+ return empty($entity) ? $factory->getLastHandler($entity_type) : $factory->getHandler($entity_type, $entity);
}
- $handlers = &$drupal_static_fast['handlers'];
-
- // Workaround the lack of a context object.
- if (empty($entity)) {
- if (isset($handlers[$entity_type]['#current'])) {
- return $handlers[$entity_type]['#current'];
- }
- elseif (empty($entity_type) && isset($handlers['#current']['#current'])) {
- return $handlers['#current']['#current'];
- }
- else {
- return NULL;
- }
- }
- elseif (is_string($entity)) {
- $entity = entity_create_stub_entity($entity_type, array(NULL, NULL, $entity));
- }
-
- list($entity_id) = entity_extract_ids($entity_type, $entity);
-
- if (!isset($handlers[$entity_type][$entity_id])) {
+ // @todo BC layer. Remove before the first stable release.
+ elseif (!empty($entity_type) && is_object($entity)) {
$entity_info = entity_get_info($entity_type);
- $class = $entity_info['translation']['entity_translation']['class'];
- // @todo remove fourth parameter once 3rd-party translation handlers have
- // been fixed and no longer require the deprecated entity_id parameter.
- $handler = new $class($entity_type, $entity_info, $entity, NULL);
-
- // If the entity id is empty we cannot cache the translation handler
- // instance.
- if (empty($entity_id)) {
- return $handler;
- }
- else {
- $handlers[$entity_type][$entity_id] = $handler;
- }
+ return new EntityTranslationDefaultHandler($entity_type, $entity_info, $entity);
}
- elseif ($update) {
- $handlers[$entity_type][$entity_id]->setEntity($entity);
- }
-
- $handlers[$entity_type]['#current'] = $handlers[$entity_type][$entity_id];
- $handlers['#current']['#current'] = $handlers[$entity_type][$entity_id];
- return $handlers[$entity_type][$entity_id];
}
/**
@@ -1813,24 +1838,11 @@ function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $up
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
-function entity_translation_entity_form_get_handler($form, &$form_state) {
+function entity_translation_entity_form_get_handler($form, $form_state) {
$handler = FALSE;
- $entity_type = isset($form['#entity_type']) && is_string($form['#entity_type']) ? $form['#entity_type'] : FALSE;
-
- if ($entity_type) {
- if (empty($form_state['storage']['entity_translation']['handler'][$entity_type])) {
- if ($info = entity_translation_edit_form_info($form, $form_state)) {
- if (entity_translation_enabled($info['entity type'], $info['entity'])) {
- $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
- $form_state['storage']['entity_translation']['handler'][$info['entity type']] = $handler;
- }
- }
- }
- else {
- $handler = $form_state['storage']['entity_translation']['handler'][$entity_type];
- }
+ if ($info = entity_translation_edit_form_info($form, $form_state)) {
+ $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
}
-
return $handler;
}
@@ -1839,14 +1851,18 @@ function entity_translation_entity_form_get_handler($form, &$form_state) {
*
* @return EntityTranslationHandlerInterface
* A translation handler instance if available, FALSE oterwise.
+ *
+ * @deprecated This is no longer used and will be removed in the first stable
+ * release.
*/
function entity_translation_current_form_get_handler() {
$handler = FALSE;
if (!empty($_POST['form_build_id'])) {
$form_state = form_state_defaults();
- $form = form_get_cache($_POST['form_build_id'], $form_state);
- $handler = entity_translation_entity_form_get_handler($form, $form_state);
+ if ($form = form_get_cache($_POST['form_build_id'], $form_state)) {
+ $handler = entity_translation_entity_form_get_handler($form, $form_state);
+ }
}
return $handler;
@@ -1871,11 +1887,13 @@ function entity_translation_edit_form_info($form, $form_state) {
if ($entity_type) {
$entity_info = entity_get_info($form['#entity_type']);
if (!empty($entity_info['translation']['entity_translation']['edit form'])) {
- $entity_key = $entity_info['translation']['entity_translation']['edit form'];
- if (isset($form_state[$entity_key])) {
+ $entity_keys = explode('][', $entity_info['translation']['entity_translation']['edit form']);
+ $key_exists = FALSE;
+ $entity = drupal_array_get_nested_value($form_state, $entity_keys, $key_exists);
+ if ($key_exists) {
$info = array(
'entity type' => $form['#entity_type'],
- 'entity' => (object) $form_state[$entity_key],
+ 'entity' => (object) $entity,
);
}
}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation_i18n_menu/entity_translation_i18n_menu.info b/sites/all/modules/contrib/localisation/entity_translation/entity_translation_i18n_menu/entity_translation_i18n_menu.info
index 4e0d9afa..40c2501a 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation_i18n_menu/entity_translation_i18n_menu.info
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation_i18n_menu/entity_translation_i18n_menu.info
@@ -7,9 +7,9 @@ dependencies[] = i18n
dependencies[] = i18n_menu
files[] = entity_translation_i18n_menu.test
-; Information added by drupal.org packaging script on 2013-07-23
-version = "7.x-1.0-beta3"
+; Information added by Drupal.org packaging script on 2015-01-22
+version = "7.x-1.0-beta4"
core = "7.x"
project = "entity_translation"
-datestamp = "1374601567"
+datestamp = "1421971088"
diff --git a/sites/all/modules/contrib/localisation/entity_translation/entity_translation_upgrade/entity_translation_upgrade.info b/sites/all/modules/contrib/localisation/entity_translation/entity_translation_upgrade/entity_translation_upgrade.info
index bd7c5173..a228c423 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/entity_translation_upgrade/entity_translation_upgrade.info
+++ b/sites/all/modules/contrib/localisation/entity_translation/entity_translation_upgrade/entity_translation_upgrade.info
@@ -4,9 +4,9 @@ package = Multilingual - Entity Translation
core = 7.x
dependencies[] = entity_translation
-; Information added by drupal.org packaging script on 2013-07-23
-version = "7.x-1.0-beta3"
+; Information added by Drupal.org packaging script on 2015-01-22
+version = "7.x-1.0-beta4"
core = "7.x"
project = "entity_translation"
-datestamp = "1374601567"
+datestamp = "1421971088"
diff --git a/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler.inc b/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler.inc
index ae7ede3e..cc0f6f3b 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler.inc
+++ b/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler.inc
@@ -14,6 +14,21 @@
*/
interface EntityTranslationHandlerInterface {
+ /**
+ * Injects the translation handler factory.
+ */
+ public function setFactory(EntityTranslationHandlerFactory $factory);
+
+ /**
+ * Registers a child translation handler for the given entity.
+ */
+ public function addChild($entity_type, $entity);
+
+ /**
+ * Removes a previously registered child translation handler.
+ */
+ public function removeChild($entity_type, $entity);
+
/**
* Loads the translation data into the wrapped entity.
*/
@@ -62,6 +77,11 @@ interface EntityTranslationHandlerInterface {
*/
public function removeTranslations();
+ /**
+ * Removes all translations from the current revision.
+ */
+ public function removeRevisionTranslations();
+
/**
* Initialize the language of the original field values.
*
@@ -98,6 +118,9 @@ interface EntityTranslationHandlerInterface {
/**
* Returns TRUE if the entity is currently being translated.
+ *
+ * @deprecated This is no longer used and will be removed before the first
+ * stable release.
*/
public function isTranslating();
@@ -106,6 +129,9 @@ interface EntityTranslationHandlerInterface {
*
* @param $translating
* A boolean value.
+ *
+ * @deprecated This is no longer used and will be removed before the first
+ * stable release.
*/
public function setTranslating($translating);
@@ -114,6 +140,11 @@ interface EntityTranslationHandlerInterface {
*/
public function isRevision();
+ /**
+ * Return TRUE if the entity type supports revisions.
+ */
+ public function isRevisionable();
+
/**
* Replaces the wrapped entity.
*
@@ -122,6 +153,22 @@ interface EntityTranslationHandlerInterface {
*/
public function setEntity($entity);
+ /**
+ * Returns the wrapped entity.
+ *
+ * @param return
+ * The wrapped entity.
+ */
+ public function getEntity();
+
+ /**
+ * Returns the wrapped entity type.
+ *
+ * @param return
+ * The wrapped entity type.
+ */
+ public function getEntityType();
+
/**
* Checks that the wrapped entity matches the give entity
*
@@ -301,6 +348,21 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
protected $entityInfo;
protected $entityId;
protected $bundle;
+ protected $revisionable;
+
+ /**
+ * The translation handler factory.
+ *
+ * @var EntityTranslationHandlerFactory
+ */
+ protected $factory;
+
+ /**
+ * The translation handler hierarchy storage.
+ *
+ * @var array
+ */
+ protected $children = array();
private $entityForm;
private $translating;
@@ -357,14 +419,29 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return;
}
+ $revisionable = self::isEntityTypeRevisionable($entity_type);
+ $revisions_ids = array();
foreach ($entities as $id => $entity) {
$entities[$id]->{$translations_key} = self::emptyTranslations();
+ if ($revisionable) {
+ list(, $revisions_id,) = entity_extract_ids($entity_type, $entity);
+ $revisions_ids[$id] = $revisions_id;
+ }
}
- $results = db_select('entity_translation', 'et')
+ $table = $revisionable ? 'entity_translation_revision' : 'entity_translation';
+ $query = db_select($table, 'et')
->fields('et')
- ->condition('entity_type', $entity_type)
- ->condition('entity_id', array_keys($entities), 'IN')
+ ->condition('entity_type', $entity_type);
+
+ if (!$revisionable) {
+ $query->condition('entity_id', array_keys($entities), 'IN');
+ }
+ else {
+ $query->condition('revision_id', $revisions_ids, 'IN');
+ }
+
+ $results = $query
->orderBy('entity_id')
->orderBy('created')
->execute();
@@ -394,6 +471,46 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return $links;
}
+ /**
+ * @see EntityTranslationHandlerInterface::setFactory()
+ */
+ public function setFactory(EntityTranslationHandlerFactory $factory) {
+ $this->factory = $factory;
+ }
+
+ /**
+ * @see EntityTranslationHandlerInterface::addChild()
+ */
+ public function addChild($entity_type, $entity) {
+ if (!empty($this->factory)) {
+ $handler = $this->factory->getHandler($entity_type, $entity);
+ $handler->setFormLanguage($this->getFormLanguage());
+ $handler->setSourceLanguage($this->getSourceLanguage());
+ // Avoid registering more than one child handler for each entity.
+ $hid = $this->factory->getHandlerId($entity_type, $entity);
+ $this->children[$hid] = $handler;
+ }
+ }
+
+ /**
+ * @see EntityTranslationHandlerInterface::removeChild()
+ */
+ public function removeChild($entity_type, $entity) {
+ if (!empty($this->factory)) {
+ $hid = $this->factory->getHandlerId($entity_type, $entity);
+ unset($this->children[$hid]);
+ }
+ }
+
+ /**
+ * Proxies the specified method invocation to a child translation handler.
+ */
+ protected function notifyChildren($method, $args) {
+ foreach ($this->children as $handler) {
+ call_user_func_array(array($handler, $method), $args);
+ }
+ }
+
/**
* @see EntityTranslationHandlerInterface::loadTranslations()
*/
@@ -410,43 +527,14 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::saveTranslations()
*/
public function saveTranslations() {
- // Delete and insert, rather than update, in case a value was added.
- db_delete('entity_translation')
- ->condition('entity_type', $this->entityType)
- ->condition('entity_id', $this->entityId)
- ->execute();
-
$translations = $this->getTranslations();
- if (count($translations->data)) {
- global $user;
+ // Save current values.
+ $this->doSaveTranslations($translations, 'entity_translation');
- $columns = array('entity_type', 'entity_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
- $query = db_insert('entity_translation')->fields($columns);
-
- // These values should overridde the translation ones as they are not
- // supposed to change.
- $overrides = array(
- 'entity_id' => $this->entityId,
- 'entity_type' => $this->entityType,
- );
-
- // These instead are just defaults.
- $defaults = array(
- 'source' => '',
- 'uid' => $user->uid,
- 'translate' => 0,
- 'status' => 0,
- 'created' => REQUEST_TIME,
- 'changed' => REQUEST_TIME,
- );
-
- foreach ($translations->data as $langcode => $translation) {
- $translation = $overrides + $translation + $defaults;
- $query->values($translation);
- }
-
- $query->execute();
+ // Save revision values.
+ if ($this->isRevisionable()) {
+ $this->doSaveTranslations($translations, 'entity_translation_revision', TRUE);
}
// The translation handler interface decouples operations on translations at
@@ -476,6 +564,54 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
+ /**
+ * Saves entity translation records to the storage.
+ */
+ protected function doSaveTranslations($translations, $table, $revision = FALSE) {
+ // Delete and insert, rather than update, in case a value was added.
+ $query = db_delete($table)
+ ->condition('entity_type', $this->entityType)
+ ->condition('entity_id', $this->entityId);
+ // If we are storing translations for the current revision or we are
+ // deleting the entity we should remove all translation data.
+ $langcode = $translations->original;
+ $hook = isset($translations->hook) ? $translations->hook : array();
+ if ($revision && $this->isRevisionable() && (empty($hook[$langcode]['hook']) || $hook[$langcode]['hook'] != 'delete')) {
+ $query->condition('revision_id', $this->revisionId);
+ }
+ $query->execute();
+
+ if (count($translations->data)) {
+ $columns = array('entity_type', 'entity_id', 'revision_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
+ $query = db_insert($table)->fields($columns);
+
+ // These values should override the translation ones as they are not
+ // supposed to change.
+ $overrides = array(
+ 'entity_type' => $this->entityType,
+ 'entity_id' => $this->entityId,
+ 'revision_id' => $this->isRevisionable() ? $this->revisionId : $this->entityId,
+ );
+
+ // These instead are just defaults.
+ $defaults = array(
+ 'source' => '',
+ 'uid' => $GLOBALS['user']->uid,
+ 'translate' => 0,
+ 'status' => 0,
+ 'created' => REQUEST_TIME,
+ 'changed' => REQUEST_TIME,
+ );
+
+ foreach ($translations->data as $translation) {
+ $translation = $overrides + $translation + $defaults;
+ $query->values($translation);
+ }
+
+ $query->execute();
+ }
+ }
+
/**
* @see EntityTranslationHandlerInterface::getTranslations()
*/
@@ -530,6 +666,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
}
+
+ $args = func_get_args();
+ $this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -600,7 +739,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if (empty($this->getTranslations()->data)) {
$this->initTranslations();
}
- elseif (!empty($langcode) && !$this->isTranslating()) {
+ elseif (!empty($langcode)) {
$this->setOriginalLanguage($langcode);
}
}
@@ -612,6 +751,17 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->removeTranslation(NULL);
}
+ /**
+ * @see EntityTranslationHandlerInterface::removeRevisionTranslations()
+ */
+ public function removeRevisionTranslations() {
+ $translations_key = $this->getTranslationsKey();
+ $keys = array_keys($this->entity->{$translations_key}->data);
+ $values = array_fill(0, count($keys), array('hook' => 'delete_revision'));
+ $this->removeTranslation(NULL);
+ $this->entity->{$translations_key}->hook = array_combine($keys, $values);
+ }
+
/**
* @see EntityTranslationHandlerInterface::initOriginalTranslation()
*/
@@ -702,13 +852,17 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
public function setOriginalLanguage($langcode) {
$translations = $this->getTranslations();
- if (isset($translations->original) && $translations->original != $langcode) {
- $translations->data[$langcode] = $translations->data[$translations->original];
- $translations->data[$langcode]['language'] = $langcode;
- unset($translations->data[$translations->original]);
- }
+ if (!isset($translations->original) || $translations->original != $langcode) {
+ if (isset($translations->original)) {
+ $translations->data[$langcode] = $translations->data[$translations->original];
+ $translations->data[$langcode]['language'] = $langcode;
+ unset($translations->data[$translations->original]);
+ }
- $translations->original = $langcode;
+ $translations->original = $langcode;
+ $args = func_get_args();
+ $this->notifyChildren(__FUNCTION__, $args);
+ }
}
/**
@@ -732,6 +886,25 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return FALSE;
}
+ /**
+ * @see EntityTranslationHandlerInterface::isRevisionable()
+ */
+ public function isRevisionable() {
+ $result = FALSE;
+ if (!isset($this->revisionable)) {
+ $result = self::isEntityTypeRevisionable($this->entityType);
+ }
+ return $result;
+ }
+
+ /**
+ * Returns whether the entity type is revisionable.
+ */
+ public static function isEntityTypeRevisionable($entity_type) {
+ $entity_info = entity_get_info($entity_type);
+ return variable_get('entity_translation_revision_enabled', FALSE) && !empty($entity_info['entity keys']['revision']);
+ }
+
/**
* @see EntityTranslationHandlerInterface::setEntity()
*/
@@ -744,8 +917,27 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->entity->{$translations_key} = self::emptyTranslations();
}
- // Update bundle and entity id properties.
- list($this->entityId, , $this->bundle) = entity_extract_ids($this->entityType, $this->entity);
+ // Update entity properties.
+ list($this->entityId, $this->revisionId, $this->bundle) = entity_extract_ids($this->entityType, $this->entity);
+
+ // Initialize the handler id if needed.
+ if (!empty($this->factory)) {
+ $this->factory->getHandlerId($this->entityType, $entity);
+ }
+ }
+
+ /**
+ * @see EntityTranslationHandlerInterface::getEntity()
+ */
+ public function getEntity() {
+ return $this->entity;
+ }
+
+ /**
+ * @see EntityTranslationHandlerInterface::getEntityType()
+ */
+ public function getEntityType() {
+ return $this->entityType;
}
/**
@@ -767,6 +959,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$translation['translate'] = 1;
}
}
+ $args = func_get_args();
+ $this->notifyChildren(__FUNCTION__, $args);
}
}
@@ -922,6 +1116,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
*/
public function setFormLanguage($langcode) {
$this->formLanguage = $langcode;
+ $args = func_get_args();
+ $this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -936,6 +1132,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
*/
public function setSourceLanguage($langcode) {
$this->sourceLanguage = $langcode;
+ $args = func_get_args();
+ $this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -974,12 +1172,16 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$languages = language_list();
$access = user_access('translate any entity') || user_access("translate $this->entityType entities");
+ // Store contextual information in the form state.
+ $form_state['entity_translation']['form_langcode'] = $form_langcode;
+ $form_state['entity_translation']['source_langcode'] = $this->getSourceLanguage();
// The only way to determine whether we are editing the original values is
// comparing form language and entity language. Since a language change
// might render impossible to make this check after form submission, we
// store the related information here.
$form_state['entity_translation']['is_translation'] = $is_translation;
+
// Adjust page title to specify the current language being edited, if we
// have at least one translation.
if ($form_langcode != LANGUAGE_NONE && (!$no_translations || $new_translation)) {
@@ -1019,7 +1221,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
// Add the entity language switcher.
$this->entityFormLanguageWidget($form, $form_state);
- if ($is_translation) {
+ if ($is_translation && isset($form['actions']['delete'])) {
// Replace the delete button with the delete translation one.
if (!$new_translation) {
$weight = 100;
@@ -1101,7 +1303,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
);
}
- $name = $new_translation ? $GLOBALS['user']->name : user_load($translations->data[$form_langcode]['uid'])->name;
+ $translation_author = $new_translation ? $GLOBALS['user'] : user_load($translations->data[$form_langcode]['uid']);
+ $name = isset($translation_author->name) ? $translation_author->name : '';
$form['translation']['name'] = array(
'#type' => 'textfield',
'#title' => t('Authored by'),
@@ -1142,8 +1345,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* Either remove access or add a translatability clue depending on the current
* user's "edit translation shared fields" permissions.
*/
- public function entityFormSharedElements(&$element) {
- static $ignored_types, $shared_labels, $access;
+ public function entityFormSharedElements(&$element, $access = NULL) {
+ static $ignored_types, $shared_labels;
+
if (!isset($ignored_types)) {
$ignored_types = array_flip(array('actions', 'value', 'hidden', 'vertical_tabs', 'token'));
}
@@ -1156,7 +1360,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach (element_children($element) as $key) {
if (!isset($element[$key]['#type'])) {
- $this->entityFormSharedElements($element[$key]);
+ $this->entityFormSharedElements($element[$key], $access);
}
else {
// Ignore non-widget form elements.
@@ -1183,7 +1387,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
public function entityFormLanguageWidget(&$form, &$form_state) {
if (entity_translation_enabled($this->entityType, $this->bundle)) {
$is_new = $this->isNewEntity();
- $is_translation = !$is_new && !empty($form_state['entity_translation']['is_translation']);
+ $is_translation = !empty($form_state['entity_translation']['is_translation']);
$translations = $this->getTranslations();
$settings = entity_translation_settings($this->entityType, $this->bundle);
$languages = entity_translation_languages($this->entityType, $this->entity);
@@ -1191,8 +1395,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach ($languages as $langcode => $language) {
// Disable languages for existing translations, so it is not possible to
- // switch this entity to some language which is already in the translation
- // set.
+ // switch this entity to some language which is already in the
+ // translation set.
if (!isset($translations->data[$langcode]) || empty($translations->data[$langcode]['source'])) {
$options[$langcode] = t($language->name);
}
@@ -1217,17 +1421,14 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
- if (!empty($form['actions']['submit']['#submit'])) {
- $submit = &$form['actions']['submit']['#submit'];
- }
- else {
- if (!isset($form['#submit'])) {
- $form['#submit'] = array();
- }
- $submit = &$form['#submit'];
- }
-
- array_unshift($submit, 'entity_translation_language_widget_submit');
+ // Prepend an empty form element to the form array so that we can update the
+ // form language before any other form handler has been called.
+ $form = array(
+ 'entity_translation_entity_form_language_update' => array(
+ '#element_validate' => array('entity_translation_entity_form_language_update'),
+ '#entity_type' => $this->entityType,
+ ),
+ ) + $form;
}
/**
@@ -1260,28 +1461,36 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
protected function updateFormLanguage($form_state) {
// Update the form language as it might have changed. We exploit the
// validation phase to be sure to act as early as possible.
- if (isset($form_state['values']['language']) && !$this->isTranslationForm()) {
- $this->setFormLanguage($form_state['values'][$this->getLanguageKey()]);
+ $language_key = $this->getLanguageKey();
+ if (isset($form_state['values'][$language_key]) && !$this->isTranslationForm()) {
+ $langcode = $form_state['values'][$language_key];
+ $this->setFormLanguage($langcode);
}
}
/**
* @see EntityTranslationHandlerInterface::entityFormLanguageWidgetSubmit()
*/
- function entityFormLanguageWidgetSubmit($form, &$form_state) {
+ public function entityFormLanguageWidgetSubmit($form, &$form_state) {
+ if (!entity_translation_enabled($this->entityType, $this->bundle)) {
+ return;
+ }
+
$this->updateFormLanguage($form_state);
$form_langcode = $this->getFormLanguage();
foreach (field_info_instances($this->entityType, $this->bundle) as $instance) {
$field_name = $instance['field_name'];
- $field = field_info_field($field_name);
- $previous_langcode = $form[$field_name]['#language'];
+ if (isset($form[$field_name]['#language'])) {
+ $field = field_info_field($field_name);
+ $previous_langcode = $form[$field_name]['#language'];
- // Handle a possible language change: new language values are inserted,
- // previous ones are deleted.
- if ($field['translatable'] && $previous_langcode != $form_langcode) {
- $form_state['values'][$field_name][$form_langcode] = $form_state['values'][$field_name][$previous_langcode];
- $form_state['values'][$field_name][$previous_langcode] = array();
+ // Handle a possible language change: new language values are inserted,
+ // previous ones are deleted.
+ if ($field['translatable'] && $previous_langcode != $form_langcode && isset($form_state['values'][$field_name][$previous_langcode])) {
+ $form_state['values'][$field_name][$form_langcode] = $form_state['values'][$field_name][$previous_langcode];
+ $form_state['values'][$field_name][$previous_langcode] = array();
+ }
}
}
}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler_factory.inc b/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler_factory.inc
new file mode 100644
index 00000000..bdbcf9be
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation/includes/translation.handler_factory.inc
@@ -0,0 +1,139 @@
+getHandlerId($entity_type, $entity);
+ if (!isset($this->handlers[$entity_type][$id])) {
+ $entity_info = entity_get_info($entity_type);
+ $class = $entity_info['translation']['entity_translation']['class'];
+ // @todo Remove the fourth parameter once 3rd-party translation handlers
+ // have been fixed and no longer require the deprecated entity_id
+ // parameter.
+ $handler = new $class($entity_type, $entity_info, $entity, NULL);
+ $handler->setFactory($this);
+ $this->handlers[$entity_type][$id] = $handler;
+ }
+
+ $this->last = $this->handlers[$entity_type][$id];
+ $this->lastByType[$entity_type] = $this->last;
+ $this->last->setEntity($entity);
+
+ return $this->last;
+ }
+
+ /**
+ * Retrieves the translation handler identifier for the given entity.
+ *
+ * @param $entity_type
+ * The type of the entity the translation handler will wrap.
+ * @param $entity
+ * The entity the translation handler will wrap.
+ */
+ public function getHandlerId($entity_type, $entity) {
+ if (!isset($entity->entity_translation_handler_id)) {
+ list($id, $revision_id) = entity_extract_ids($entity_type, $entity);
+ $revision_id = isset($revision_id) ? $revision_id : 0;
+ $entity->entity_translation_handler_id = $entity_type . '-' . (!empty($id) ? 'eid-' . $id . '-' . $revision_id : 'new-' . self::$newId++);
+ }
+ return $entity->entity_translation_handler_id;
+ }
+
+ /**
+ * Returns the last translation handler retrieved.
+ *
+ * @param $entity_type
+ * (optional) The entity type of the translation handler. Defaults to the
+ * last one.
+ *
+ * @return EntityTranslationHandlerInterface
+ * A class implementing EntityTranslationHandlerInterface.
+ */
+ public function getLastHandler($entity_type = NULL) {
+ if (isset($entity_type)) {
+ return isset($this->lastByType[$entity_type]) ? $this->lastByType[$entity_type] : NULL;
+ }
+ return $this->last;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/plugins/access/translation_exists.inc b/sites/all/modules/contrib/localisation/entity_translation/plugins/access/translation_exists.inc
new file mode 100644
index 00000000..a87b384b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation/plugins/access/translation_exists.inc
@@ -0,0 +1,142 @@
+ t("Entity translation: translation exists"),
+ 'description' => t('Control access by checking if a translation exists.'),
+ 'callback' => 'entity_translation_translation_exists_ctools_access_check',
+ 'default' => array('language' => array()),
+ 'settings form' => 'entity_translation_translation_exists_ctools_access_settings',
+ 'settings form submit' => 'entity_translation_translation_exists_ctools_access_settings_submit',
+ 'summary' => 'entity_translation_translation_exists_ctools_access_summary',
+ 'get child' => 'entity_translation_translation_exists_ctools_access_get_child',
+ 'get children' => 'entity_translation_translation_exists_ctools_access_get_children',
+);
+
+/**
+ * Get a particular instance of this plugin.
+ */
+function entity_translation_translation_exists_ctools_access_get_child($plugin, $parent, $child) {
+
+ $plugins = &drupal_static(__FUNCTION__, array());
+ if (empty($plugins[$parent . ':' . $child])) {
+ $plugins[$parent . ':' . $child] = _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $child);
+ }
+ return $plugins[$parent . ':' . $child];
+}
+
+/**
+ * Get all children of this plugin.
+ */
+function entity_translation_translation_exists_ctools_access_get_children($plugin, $parent) {
+ $plugins = &drupal_static(__FUNCTION__, array());
+ if (!empty($plugins)) {
+ return $plugins;
+ }
+ $entities = entity_get_info();
+ foreach ($entities as $entity_type => $entity) {
+ $plugin = _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $entity_type);
+ $plugins[$parent . ':' . $entity_type] = $plugin;
+ }
+ return $plugins;
+}
+
+/**
+ * Plugin definition for one particular plugin child.
+ */
+function _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $entity_type) {
+ $entity = entity_get_info($entity_type);
+
+ $plugin['title'] = t('@entity: entity translation exists', array('@entity' => $entity['label']));
+ $plugin['keyword'] = $entity_type;
+ $plugin['description'] = t('Control access by @entity language', array('@entity' => $entity['label']));
+ $plugin['name'] = $parent . ':' . $entity_type;
+ $plugin['required context'] = new ctools_context_required(t(ucfirst($entity_type)), $entity_type);
+
+ return $plugin;
+}
+
+/**
+ * Settings form for the 'by node_language' access plugin
+ */
+function entity_translation_translation_exists_ctools_access_settings($form, &$form_state, $conf) {
+ $options = array(
+ ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current content language'),
+ ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default site language'),
+ LANGUAGE_NONE => t('Language neutral'),
+ );
+ $options = array_merge($options, locale_language_list());
+ $form['settings']['language'] = array(
+ '#title' => t('Language'),
+ '#type' => 'checkboxes',
+ '#options' => $options,
+ '#description' => t('This rule will pass if any of these languages are present.'),
+ '#default_value' => $conf['language'],
+ );
+ return $form;
+}
+
+/**
+ * Grant access based on the which translations are available.
+ */
+function entity_translation_translation_exists_ctools_access_check($conf, $context) {
+ // Check that the context exists.
+ if (empty($context) || empty($context->data)) {
+ return FALSE;
+ }
+ $entity = $context->data;
+ $handler = entity_translation_get_handler($context->keyword, $entity);
+ if (!empty($handler)) {
+ $translations = $handler->getTranslations();
+ global $language_content;
+ foreach ($conf['language'] as $lang) {
+ if ($lang) {
+ switch ($lang) {
+ case ENTITY_TRANSLATION_LANGUAGE_CURRENT:
+ $lang = $language_content->language;
+ break;
+ case ENTITY_TRANSLATION_LANGUAGE_DEFAULT:
+ $lang = language_default('language');
+ break;
+ }
+ if (isset($translations->data[$lang]) && $translations->data[$lang]['status']) {
+ return TRUE;
+ }
+ }
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * Provide a summary description based upon the checked node_languages.
+ */
+function entity_translation_translation_exists_ctools_access_summary($conf, $context) {
+ $languages = array(
+ ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current site content language'),
+ ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default site language'),
+ LANGUAGE_NONE => t('Language neutral'),
+ );
+ $languages = array_merge($languages, locale_language_list());
+
+ if (!isset($conf['language'])) {
+ $conf['language'] = array();
+ }
+
+ $names = array();
+ foreach (array_filter($conf['language']) as $language) {
+ $names[] = $languages[$language];
+ }
+
+ if (empty($names)) {
+ return t('@identifier is in any language', array('@identifier' => $context->identifier));
+ }
+
+ return format_plural(count($names), '@languages translation exists for @identifier', '@languages translations exists for identifier', array('@languages' => implode(', ', $names), '@identifier' => $context->identifier));
+}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation.test b/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation.test
index 6a1bdc46..2a647be8 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation.test
+++ b/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation.test
@@ -234,7 +234,7 @@ class EntityTranslationTestCase extends DrupalWebTestCase {
$this->drupalGet('node/' . $node->nid . '/edit/add/' . $source_langcode . '/' . $langcode);
$body_key = "body[$langcode][0][value]";
- $this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[$node->language][0]['value'], 'Original body value correctly populated.');
+ $this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[$source_langcode][0]['value'], 'Original body value correctly populated.');
$this->assertFieldById('edit-body-' . $langcode . '-add-more', t('Add another item'), t('Add another item button found.'));
$edit = array();
@@ -477,7 +477,7 @@ class EntityTranslationHookTestCase extends EntityTranslationTestCase {
$handler->setTranslation($translation);
$handler->saveTranslations();
$node = node_load($node->nid, NULL, TRUE);
- $handler = entity_translation_get_handler('node', $node, TRUE);
+ $handler = entity_translation_get_handler('node', $node);
$translations = $handler->getTranslations();
$this->assertTrue(!empty($translations->data['it']), t('An Italian translation has been created'));
$info = $this->getHookInfo();
@@ -497,7 +497,7 @@ class EntityTranslationHookTestCase extends EntityTranslationTestCase {
$handler->setTranslation($translation);
$handler->saveTranslations();
$node = node_load($node->nid, NULL, TRUE);
- $handler = entity_translation_get_handler('node', $node, TRUE);
+ $handler = entity_translation_get_handler('node', $node);
$translations = $handler->getTranslations();
$this->assertTrue(!empty($translations->data['es']), t('A Spanish translation has been created'));
$info = $this->getHookInfo();
diff --git a/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation_test.info b/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation_test.info
index b49cb7fb..3ae19cbe 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation_test.info
+++ b/sites/all/modules/contrib/localisation/entity_translation/tests/entity_translation_test.info
@@ -6,9 +6,9 @@ hidden = TRUE
dependencies[] = entity_translation
files[] = entity_translation_test.module
-; Information added by drupal.org packaging script on 2013-07-23
-version = "7.x-1.0-beta3"
+; Information added by Drupal.org packaging script on 2015-01-22
+version = "7.x-1.0-beta4"
core = "7.x"
project = "entity_translation"
-datestamp = "1374601567"
+datestamp = "1421971088"
diff --git a/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation.views.inc b/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation.views.inc
index 3353c1d9..e6b5cf8a 100644
--- a/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation.views.inc
+++ b/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation.views.inc
@@ -224,4 +224,24 @@ function entity_translation_views_data_alter(&$data) {
);
}
}
+
+ // Expose all translatable fields, using a handler based off Views' default
+ // field handler, that allows users to select the language to display the
+ // field in.
+ foreach (field_info_fields() as $field) {
+ if ($field['translatable'] && $field['storage']['type'] == 'field_sql_storage') {
+ // Set defaults and just change the handler, title, group, and help.
+ $defaults = field_views_field_default_views_data($field);
+ foreach ($defaults as $table_name => $table_data) {
+ if (isset($data[$table_name][$field['field_name']])) {
+ $field_title = $table_data[$field['field_name']]['title'];
+ $table_data[$field['field_name']]['title'] = t('!title: translated', array('!title' => $field_title));
+ $table_data[$field['field_name']]['group'] = t('Entity translation');
+ $table_data[$field['field_name']]['help'] .= ' ' . t('Show the field !title translated into a specified language', array('!title' => $field_title));
+ $table_data[$field['field_name']]['field']['handler'] = 'entity_translation_handler_field_field';
+ $data[$table_name][$field['field_name'] . '_et'] = $table_data[$field['field_name']];
+ }
+ }
+ }
+ }
}
diff --git a/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation_handler_field_field.inc b/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation_handler_field_field.inc
new file mode 100644
index 00000000..a132e0d3
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation/views/entity_translation_handler_field_field.inc
@@ -0,0 +1,65 @@
+ '***CURRENT_LANGUAGE***',
+ );
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $languages = array(
+ '***CURRENT_LANGUAGE***' => t("Current user's language"),
+ '***DEFAULT_LANGUAGE***' => t("Default site language"),
+ LANGUAGE_NONE => t('Language neutral'),
+ );
+
+ $languages = array_merge($languages, locale_language_list());
+
+ $form['language'] = array(
+ '#type' => 'select',
+ '#title' => t('Language'),
+ '#options' => $languages,
+ '#default_value' => $this->options['language'],
+ '#description' => t('Select the language to display this field in')
+ );
+ }
+
+ /**
+ * Overrides parent::field_language, retrieving the language from the handler
+ * options.
+ */
+ function field_language($entity_type, $entity) {
+ global $language_content;
+
+ if (field_is_translatable($entity_type, $this->field_info)) {
+ $default_language = language_default('language');
+ $language = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'),
+ array($language_content->language, $default_language),
+ $this->options['language']);
+
+ // Give the Field Language API a chance to fallback to a different language
+ // (or LANGUAGE_NONE), in case the field has no data for the selected language.
+ // field_view_field() does this as well, but since the returned language code
+ // is used before calling it, the fallback needs to happen explicitly.
+ $language = field_language($entity_type, $entity, $this->field_info['field_name'], $language);
+
+ return $language;
+ }
+ else {
+ return LANGUAGE_NONE;
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/README.txt b/sites/all/modules/contrib/localisation/l10n_update/README.txt
index 3905b76e..feb75cb2 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/README.txt
+++ b/sites/all/modules/contrib/localisation/l10n_update/README.txt
@@ -26,42 +26,43 @@ Installation
Download, unpack the module the usual way.
Enable this module and the Locale module (core).
- You need at least one language other than English.
- On Administration > Configuration > Regional and language:
- Click "Add language"
- Pull-down menu: Choose your new language
- Then click "Add language"
+ You need at least one language (besides the default English).
+ On Administration > Configuration > Regional and language > Languages:
+ Click "Add language".
+ Select a language from the select list "Language name".
+ Then click the "Add language" button.
Drupal is now importing interface translations. This can take a few minutes.
- When it's finished, you'll get a confirmation with a summary of all
- translation files that have been pulled in.
+ When it's finished, you'll get a confirmation with a summary of the
+ translations that have been imported.
If required, enable the new language as default language.
- Home > Administration > Configuration > Regional and language:
- Select your new language as default
+ Administration > Configuration > Regional and language > Languages:
+ Select your new default language.
Update interface translations
-----------------------------
- On Home > Administration > Configuration > Regional and language:
- Choose the "Translation updates" tab
- Change "Check for updates" to Daily or Weekly instead of the default "Never".
+ You want to import translations regularly using cron. You can enable this
+ on Administration > Configuration > Regional and language > Languages:
+ Choose the "Translation updates" tab.
+ Change "Check for updates" to "Daily" or "Weekly" instead of the default "Never".
+ From now on cron will check for updated translations, and import them is required.
- Cron will from now on check for updated translations, and will report the
- update status on the status page (Home > Administration > Reports).
+ The status of the translations is reported on the "Status report" page at
+ Administration > Reports.
To check the translation status and execute updates manually, go to
- Administration > Configuration > Regional and language > Translate inteface
- Here you see English and your new language.
- Choose the "Update" tab
+ Administration > Configuration > Regional and language > Translate inteface
+ Choose the "Update" tab.
You see a list of all modules and their translation status.
On the bottom of the page, you can manually update using "Update translations".
Use Drush
---------
You can also use drush to update your translations:
- drush l10n-update # Update translations.
- drush l10n-update-refresh # Refresh available information.
- drush l10n-update-status # Show translation status of available project
+ drush l10n-update # Update translations.
+ drush l10n-update-refresh # Refresh available information.
+ drush l10n-update-status # Show translation status of available project
Summary of administrative pages
@@ -114,27 +115,37 @@ po files, multi site and distributions
Po files included in distributions should match this syntax too.
-Alternative sources of translation
-----------------------------------
-
- Each project i.e. modules, themes, etc. can define alternative translation
- servers to retrieve the translation updates from.
- Include the following definition in the projects .info file:
-
- l10n server = example.com
- l10n url = http://example.com/files/translations/l10n_server.xml
+Alternative source of translation
+---------------------------------
The download path pattern is normally defined in the above defined xml file.
- You may override this path by adding a third definition in the .info file:
+ You may override the download path of the po file on a project by project
+ basis by adding this definition in the .info file:
l10n path = http://example.com/files/translations/%core/%project/%project-%release.%language.po
+ Modules can force Locale to load the translation of an other project by
+ defining 'interface translation project' in their .info file. This can be
+ usefull for custom modules to use for example a common translation file
+
+ interface translation project = my_project
+
+ This can be used in combination with an alternative path to the translation
+ file. For example:
+
+ l10n path = sites/all/modules/custom/%project/%project.%language.po
+
+Exclude a project from translation checks and updates
+-----------------------------------------------------
+
+ Individual modules can be excluded from translation checks and updates. For
+ example custom modules or features. Add the following line to the .info file
+ to exclude a module from translation checks and updates:
+
+ interface translation project = FALSE
+
API
---
- Using hook_l10n_servers the l10n update module can be extended to use other
- translation repositories. Which is usefull for organisations who maintain
- their own translation.
-
Using hook_l10n_update_projects_alter modules can alter or specify the
translation repositories on a per module basis.
@@ -142,6 +153,6 @@ API
Maintainers
-----------
- Jose Reyero
- Gábor Hojtsy
Erik Stielstra
+ Gábor Hojtsy
+ Jose Reyero
diff --git a/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin-rtl.css b/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin-rtl.css
index b37baa63..f17a0ff2 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin-rtl.css
+++ b/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin-rtl.css
@@ -1,25 +1,11 @@
-/* Expand/collapse image for project title */
-html.js .l10n-update-wrapper .project-legend {
- padding-right: 10px;
+/**
+ * Available translation updates page.
+ */
+#l10n-update-status-form .expand .inner {
+ background: transparent url(../images/menu-collapsed-rtl.png) right .6em no-repeat;
+ margin-right: -12px;
+ padding-right: 12px;
}
-html.js .l10n-update-wrapper.collapsed .project-legend {
- background: url("../images/menu-collapsed-rtl.png") right 50% no-repeat;
-}
-
-html.js .l10n-update-wrapper .project-legend a {
- margin-right: -10px;
- padding-right: 10px;
-}
-
-/* Translation update status data */
-html.js .l10n-update-wrapper.collapsed .project .version-status {
- float: left;
-}
-
-.l10n-update .version-status {
- float: left;
-}
-
-.l10n-update .version-links {
- float: left;
+#l10n-update-status-form .expanded .expand .inner {
+ background: transparent url(../images/menu-expanded.png) right .6em no-repeat;
}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin.css b/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin.css
index c42dca31..bc08bc9c 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin.css
+++ b/sites/all/modules/contrib/localisation/l10n_update/css/l10n_update.admin.css
@@ -1,46 +1,82 @@
-html.js .l10n-update-wrapper.collapsed .fieldset-wrapper {
- display: none;
+/**
+ * Available translation updates page.
+ */
+#l10n-update-status-form table {
+ table-layout: fixed;
+}
+#l10n-update-status-form th.select-all {
+ width: 4%;
+}
+#l10n-update-status-form th.title {
+ width: 25%;
+}
+#l10n-update-status-form th.description {
+}
+#l10n-update-status-form td {
+ vertical-align: top;
+}
+#l10n-update-status-form .expand .inner {
+ background: transparent url(../images/menu-collapsed.png) left .6em no-repeat;
+ margin-left: -12px;
+ padding-left: 12px;
+}
+#l10n-update-status-form .expanded .expand .inner {
+ background: transparent url(../images/menu-expanded.png) left .6em no-repeat;
}
-.l10n-update-wrapper .project .version-status {
- display: none;
+#l10n-update-status-form .label {
+ color: #1d1d1d;
+ font-size: 1.15em;
+ font-weight: bold;
}
-
-/* Expand/collapse image for project title */
-html.js .l10n-update-wrapper .project-title {
- background: url("../images/menu-expanded.png") left 65% no-repeat;
- padding-left: 10px;
+#l10n-update-status-form .description {
+ cursor: pointer;
}
-
-html.js .l10n-update-wrapper.collapsed .project-title {
- background: url("../images/menu-collapsed.png") left 50% no-repeat;
+#l10n-update-status-form .description .inner {
+ color: #5c5c5b;
+ line-height: 20px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-
-html.js .l10n-update-wrapper .project-title a {
- margin-left: -10px;
- padding-left: 10px;
+#l10n-update-status-form .expanded .description .inner {
+ height: auto;
+ overflow: visible;
+ white-space: normal;
}
-
-/* Translation update status data */
-html.js .l10n-update-wrapper.collapsed .project .version-status {
- display: inline;
- float: right;
+#l10n-update-status-form .expanded .description .text {
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
}
-
-.l10n-update .project-server {
- margin: 0 10px;
- font-size: 90%;
- font-weight: normal
+.js #l10n-update-status-form .description .inner {
+ height: 20px;
}
-
-.l10n-update .version-status {
- float: right;
- font-size: 90%;
- font-weight: normal;
+#l10n-update-status-form .expanded .description .inner {
+ height: auto;
+ overflow: visible;
+ white-space: normal;
}
-
-.l10n-update .version-links {
- float: right;
- padding-right: 1em;
-
+#l10n-update-status-form .details {
+ padding: 5px 0;
+ max-width: 490px;
+ white-space: normal;
+ font-size: 0.9em;
+ color: #666;
+}
+#l10n-update-status-form .visually-hidden {
+ position: absolute !important;
+ clip: rect(1px, 1px, 1px, 1px);
+ overflow: hidden;
+ height: 1px;
+ width: 1px;
+ word-wrap: normal;
+}
+@media screen and (max-width: 40em) {
+ #l10n-update-status-form th.title {
+ width: 20%;
+ }
+ #l10n-update-status-form th.status {
+ width: 40%;
+ }
}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoHeader.php b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoHeader.php
new file mode 100644
index 00000000..2367f059
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoHeader.php
@@ -0,0 +1,418 @@
+1);\n"
+ */
+class PoHeader {
+
+ /**
+ * Language code.
+ *
+ * @var string
+ */
+ private $_langcode;
+
+ /**
+ * Formula for the plural form.
+ *
+ * @var string
+ */
+ private $_pluralForms;
+
+ /**
+ * Author(s) of the file.
+ *
+ * @var string
+ */
+ private $_authors;
+
+ /**
+ * Date the po file got created.
+ *
+ * @var string
+ */
+ private $_po_date;
+
+ /**
+ * Human readable language name.
+ *
+ * @var string
+ */
+ private $_languageName;
+
+ /**
+ * Name of the project the translation belongs to.
+ *
+ * @var string
+ */
+ private $_projectName;
+
+ /**
+ * Constructor, creates a PoHeader with default values.
+ *
+ * @param string $langcode
+ * Language code.
+ */
+ public function __construct($langcode = NULL) {
+ $this->_langcode = $langcode;
+ // Ignore errors when run during site installation before
+ // date_default_timezone_set() is called.
+ $this->_po_date = @date("Y-m-d H:iO");
+ $this->_pluralForms = 'nplurals=2; plural=(n > 1);';
+ }
+
+ /**
+ * Get the plural form.
+ *
+ * @return string
+ * Plural form component from the header, for example:
+ * 'nplurals=2; plural=(n > 1);'.
+ */
+ function getPluralForms() {
+ return $this->_pluralForms;
+ }
+
+ /**
+ * Set the human readable language name.
+ *
+ * @param string $languageName
+ * Human readable language name.
+ */
+ function setLanguageName($languageName) {
+ $this->_languageName = $languageName;
+ }
+
+ /**
+ * Get the human readable language name.
+ *
+ * @return string
+ * The human readable language name.
+ */
+ function getLanguageName() {
+ return $this->_languageName;
+ }
+
+ /**
+ * Set the project name.
+ *
+ * @param string $projectName
+ * Human readable project name.
+ */
+ function setProjectName($projectName) {
+ $this->_projectName = $projectName;
+ }
+
+ /**
+ * Get the project name.
+ *
+ * @return string
+ * The human readable project name.
+ */
+ function getProjectName() {
+ return $this->_projectName;
+ }
+
+ /**
+ * Populate internal values from a string.
+ *
+ * @param string $header
+ * Full header string with key-value pairs.
+ */
+ public function setFromString($header) {
+ // Get an array of all header values for processing.
+ $values = $this->parseHeader($header);
+
+ // There is only one value relevant for our header implementation when
+ // reading, and that is the plural formula.
+ if (!empty($values['Plural-Forms'])) {
+ $this->_pluralForms = $values['Plural-Forms'];
+ }
+ }
+
+ /**
+ * Generate a Gettext PO formatted header string based on data set earlier.
+ */
+ public function __toString() {
+ $output = '';
+
+ $isTemplate = empty($this->_languageName);
+
+ $output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->_languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->_projectName) . "\n";
+ if (!empty($this->_authors)) {
+ $output .= '# Generated by ' . implode("\n# ", $this->_authors) . "\n";
+ }
+ $output .= "#\n";
+
+ // Add the actual header information.
+ $output .= "msgid \"\"\n";
+ $output .= "msgstr \"\"\n";
+ $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
+ $output .= "\"POT-Creation-Date: " . $this->_po_date . "\\n\"\n";
+ $output .= "\"PO-Revision-Date: " . $this->_po_date . "\\n\"\n";
+ $output .= "\"Last-Translator: NAME \\n\"\n";
+ $output .= "\"Language-Team: LANGUAGE \\n\"\n";
+ $output .= "\"MIME-Version: 1.0\\n\"\n";
+ $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
+ $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
+ $output .= "\"Plural-Forms: " . $this->_pluralForms . "\\n\"\n";
+ $output .= "\n";
+
+ return $output;
+ }
+
+ /**
+ * Parses a Plural-Forms entry from a Gettext Portable Object file header.
+ *
+ * @param string $pluralforms
+ * The Plural-Forms entry value.
+ *
+ * @return
+ * An array containing the number of plural forms and the converted version
+ * of the formula that can be evaluated with PHP later.
+ */
+ function parsePluralForms($pluralforms) {
+ // First, delete all whitespace.
+ $pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
+
+ // Select the parts that define nplurals and plural.
+ $nplurals = strstr($pluralforms, "nplurals=");
+ if (strpos($nplurals, ";")) {
+ // We want the string from the 10th char, because "nplurals=" length is 9.
+ $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
+ }
+ else {
+ return FALSE;
+ }
+ $plural = strstr($pluralforms, "plural=");
+ if (strpos($plural, ";")) {
+ // We want the string from the 8th char, because "plural=" length is 7.
+ $plural = substr($plural, 7, strpos($plural, ";") - 7);
+ }
+ else {
+ return FALSE;
+ }
+
+ // Get PHP version of the plural formula.
+ $plural = $this->parseArithmetic($plural);
+
+ if ($plural !== FALSE) {
+ return array($nplurals, $plural);
+ }
+ else {
+ throw new Exception('The plural formula could not be parsed.');
+ }
+ }
+
+ /**
+ * Parses a Gettext Portable Object file header.
+ *
+ * @param string $header
+ * A string containing the complete header.
+ *
+ * @return array
+ * An associative array of key-value pairs.
+ */
+ private function parseHeader($header) {
+ $header_parsed = array();
+ $lines = array_map('trim', explode("\n", $header));
+ foreach ($lines as $line) {
+ if ($line) {
+ list($tag, $contents) = explode(":", $line, 2);
+ $header_parsed[trim($tag)] = trim($contents);
+ }
+ }
+ return $header_parsed;
+ }
+
+ /**
+ * Parses and sanitizes an arithmetic formula into a PHP expression.
+ *
+ * While parsing, we ensure, that the operators have the right
+ * precedence and associativity.
+ *
+ * @param string $string
+ * A string containing the arithmetic formula.
+ *
+ * @return
+ * A version of the formula to evaluate with PHP later.
+ */
+ private function parseArithmetic($string) {
+ // Operator precedence table.
+ $precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
+ // Right associativity.
+ $right_associativity = array("?" => 1, ":" => 1);
+
+ $tokens = $this->tokenizeFormula($string);
+
+ // Parse by converting into infix notation then back into postfix
+ // Operator stack - holds math operators and symbols.
+ $operator_stack = array();
+ // Element Stack - holds data to be operated on.
+ $element_stack = array();
+
+ foreach ($tokens as $token) {
+ $current_token = $token;
+
+ // Numbers and the $n variable are simply pushed into $element_stack.
+ if (is_numeric($token)) {
+ $element_stack[] = $current_token;
+ }
+ elseif ($current_token == "n") {
+ $element_stack[] = '$n';
+ }
+ elseif ($current_token == "(") {
+ $operator_stack[] = $current_token;
+ }
+ elseif ($current_token == ")") {
+ $topop = array_pop($operator_stack);
+ while (isset($topop) && ($topop != "(")) {
+ $element_stack[] = $topop;
+ $topop = array_pop($operator_stack);
+ }
+ }
+ elseif (!empty($precedence[$current_token])) {
+ // If it's an operator, then pop from $operator_stack into
+ // $element_stack until the precedence in $operator_stack is less
+ // than current, then push into $operator_stack.
+ $topop = array_pop($operator_stack);
+ while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
+ $element_stack[] = $topop;
+ $topop = array_pop($operator_stack);
+ }
+ if ($topop) {
+ // Return element to top.
+ $operator_stack[] = $topop;
+ }
+ // Parentheses are not needed.
+ $operator_stack[] = $current_token;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ // Flush operator stack.
+ $topop = array_pop($operator_stack);
+ while ($topop != NULL) {
+ $element_stack[] = $topop;
+ $topop = array_pop($operator_stack);
+ }
+
+ // Now extract formula from stack.
+ $previous_size = count($element_stack) + 1;
+ while (count($element_stack) < $previous_size) {
+ $previous_size = count($element_stack);
+ for ($i = 2; $i < count($element_stack); $i++) {
+ $op = $element_stack[$i];
+ if (!empty($precedence[$op])) {
+ $f = "";
+ if ($op == ":") {
+ $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
+ }
+ elseif ($op == "?") {
+ $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
+ }
+ else {
+ $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
+ }
+ array_splice($element_stack, $i - 2, 3, $f);
+ break;
+ }
+ }
+ }
+
+ // If only one element is left, the number of operators is appropriate.
+ if (count($element_stack) == 1) {
+ return $element_stack[0];
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Tokenize the formula.
+ *
+ * @param string $formula
+ * A string containing the arithmetic formula.
+ *
+ * @return array
+ * List of arithmetic tokens identified in the formula.
+ */
+ private function tokenizeFormula($formula) {
+ $formula = str_replace(" ", "", $formula);
+ $tokens = array();
+ for ($i = 0; $i < strlen($formula); $i++) {
+ if (is_numeric($formula[$i])) {
+ $num = $formula[$i];
+ $j = $i + 1;
+ while ($j < strlen($formula) && is_numeric($formula[$j])) {
+ $num .= $formula[$j];
+ $j++;
+ }
+ $i = $j - 1;
+ $tokens[] = $num;
+ }
+ elseif ($pos = strpos(" =<>!&|", $formula[$i])) {
+ $next = $formula[$i + 1];
+ switch ($pos) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ if ($next == '=') {
+ $tokens[] = $formula[$i] . '=';
+ $i++;
+ }
+ else {
+ $tokens[] = $formula[$i];
+ }
+ break;
+ case 5:
+ if ($next == '&') {
+ $tokens[] = '&&';
+ $i++;
+ }
+ else {
+ $tokens[] = $formula[$i];
+ }
+ break;
+ case 6:
+ if ($next == '|') {
+ $tokens[] = '||';
+ $i++;
+ }
+ else {
+ $tokens[] = $formula[$i];
+ }
+ break;
+ }
+ }
+ else {
+ $tokens[] = $formula[$i];
+ }
+ }
+ return $tokens;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoItem.php b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoItem.php
new file mode 100644
index 00000000..2d56c2b8
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoItem.php
@@ -0,0 +1,282 @@
+_langcode;
+ }
+
+ /**
+ * Set the language code of the current language.
+ *
+ * @param string $langcode
+ */
+ function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ /**
+ * Get the context this translation belongs to.
+ *
+ * @return string $context
+ */
+ function getContext() {
+ return $this->_context;
+ }
+
+ /**
+ * Set the context this translation belongs to.
+ *
+ * @param string $context
+ */
+ function setContext($context) {
+ $this->_context = $context;
+ }
+
+ /**
+ * Get the source string or the array of strings if the translation has
+ * plurals.
+ *
+ * @return string or array $translation
+ */
+ function getSource() {
+ return $this->_source;
+ }
+
+ /**
+ * Set the source string or the array of strings if the translation has
+ * plurals.
+ *
+ * @param string or array $source
+ */
+ function setSource($source) {
+ $this->_source = $source;
+ }
+
+ /**
+ * Get the translation string or the array of strings if the translation has
+ * plurals.
+ *
+ * @return string or array $translation
+ */
+ function getTranslation() {
+ return $this->_translation;
+ }
+
+ /**
+ * Set the translation string or the array of strings if the translation has
+ * plurals.
+ *
+ * @param string or array $translation
+ */
+ function setTranslation($translation) {
+ $this->_translation = $translation;
+ }
+
+ /**
+ * Set if the translation has plural values.
+ *
+ * @param boolean $plural
+ */
+ function setPlural($plural) {
+ $this->_plural = $plural;
+ }
+
+ /**
+ * Get if the translation has plural values.
+ *
+ * @return boolean $plural
+ */
+ function isPlural() {
+ return $this->_plural;
+ }
+
+ /**
+ * Get the comment of this translation.
+ *
+ * @return String $comment
+ */
+ function getComment() {
+ return $this->_comment;
+ }
+
+ /**
+ * Set the comment of this translation.
+ *
+ * @param String $comment
+ */
+ function setComment($comment) {
+ $this->_comment = $comment;
+ }
+
+ /**
+ * Create the PoItem from a structured array.
+ *
+ * @param array values
+ */
+ public function setFromArray(array $values = array()) {
+ if (isset($values['context'])) {
+ $this->setContext($values['context']);
+ }
+ if (isset($values['source'])) {
+ $this->setSource($values['source']);
+ }
+ if (isset($values['translation'])) {
+ $this->setTranslation($values['translation']);
+ }
+ if (isset($values['comment'])){
+ $this->setComment($values['comment']);
+ }
+ if (isset($this->_source) &&
+ strpos($this->_source, L10N_UPDATE_PLURAL_DELIMITER) !== FALSE) {
+ $this->setSource(explode(L10N_UPDATE_PLURAL_DELIMITER, $this->_source));
+ $this->setTranslation(explode(L10N_UPDATE_PLURAL_DELIMITER, $this->_translation));
+ $this->setPlural(count($this->_translation) > 1);
+ }
+ }
+
+ /**
+ * Output the PoItem as a string.
+ */
+ public function __toString() {
+ return $this->formatItem();
+ }
+
+ /**
+ * Format the POItem as a string.
+ */
+ private function formatItem() {
+ $output = '';
+
+ // Format string context.
+ if (!empty($this->_context)) {
+ $output .= 'msgctxt ' . $this->formatString($this->_context);
+ }
+
+ // Format translation.
+ if ($this->_plural) {
+ $output .= $this->formatPlural();
+ }
+ else {
+ $output .= $this->formatSingular();
+ }
+
+ // Add one empty line to separate the translations.
+ $output .= "\n";
+
+ return $output;
+ }
+
+ /**
+ * Formats a plural translation.
+ */
+ private function formatPlural() {
+ $output = '';
+
+ // Format source strings.
+ $output .= 'msgid ' . $this->formatString($this->_source[0]);
+ $output .= 'msgid_plural ' . $this->formatString($this->_source[1]);
+
+ foreach ($this->_translation as $i => $trans) {
+ if (isset($this->_translation[$i])) {
+ $output .= 'msgstr[' . $i . '] ' . $this->formatString($trans);
+ }
+ else {
+ $output .= 'msgstr[' . $i . '] ""' . "\n";
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Formats a singular translation.
+ */
+ private function formatSingular() {
+ $output = '';
+ $output .= 'msgid ' . $this->formatString($this->_source);
+ $output .= 'msgstr ' . (isset($this->_translation) ? $this->formatString($this->_translation) : '""');
+ return $output;
+ }
+
+ /**
+ * Formats a string for output on multiple lines.
+ */
+ private function formatString($string) {
+ // Escape characters for processing.
+ $string = addcslashes($string, "\0..\37\\\"");
+
+ // Always include a line break after the explicit \n line breaks from
+ // the source string. Otherwise wrap at 70 chars to accommodate the extra
+ // format overhead too.
+ $parts = explode("\n", wordwrap(str_replace('\n', "\\n\n", $string), 70, " \n"));
+
+ // Multiline string should be exported starting with a "" and newline to
+ // have all lines aligned on the same column.
+ if (count($parts) > 1) {
+ return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n";
+ }
+ // Single line strings are output on the same line.
+ else {
+ return "\"$parts[0]\"\n";
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoMemoryWriter.php b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoMemoryWriter.php
new file mode 100644
index 00000000..18e07915
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoMemoryWriter.php
@@ -0,0 +1,90 @@
+_items = array();
+ }
+
+ /**
+ * Implements PoWriterInterface::writeItem().
+ */
+ public function writeItem(PoItem $item) {
+ if (is_array($item->getSource())) {
+ $item->setSource(implode(L10N_UPDATE_PLURAL_DELIMITER, $item->getSource()));
+ $item->setTranslation(implode(L10N_UPDATE_PLURAL_DELIMITER, $item->getTranslation()));
+ }
+ $context = $item->getContext();
+ $this->_items[$context != NULL ? $context : ''][$item->getSource()] = $item->getTranslation();
+ }
+
+ /**
+ * Implements PoWriterInterface::writeItems().
+ */
+ public function writeItems(PoReaderInterface $reader, $count = -1) {
+ $forever = $count == -1;
+ while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
+ $this->writeItem($item);
+ }
+ }
+
+ /**
+ * Get all stored PoItem's.
+ *
+ * @return array PoItem
+ */
+ public function getData() {
+ return $this->_items;
+ }
+
+ /**
+ * Implements Drupal\Component\Gettext\PoMetadataInterface:setLangcode().
+ *
+ * Not implemented. Not relevant for the MemoryWriter.
+ */
+ function setLangcode($langcode) {
+ }
+
+ /**
+ * Implements Drupal\Component\Gettext\PoMetadataInterface:getLangcode().
+ *
+ * Not implemented. Not relevant for the MemoryWriter.
+ */
+ function getLangcode() {
+ }
+
+ /**
+ * Implements Drupal\Component\Gettext\PoMetadataInterface:getHeader().
+ *
+ * Not implemented. Not relevant for the MemoryWriter.
+ */
+ function getHeader() {
+ }
+
+ /**
+ * Implements Drupal\Component\Gettext\PoMetadataInterface:setHeader().
+ *
+ * Not implemented. Not relevant for the MemoryWriter.
+ */
+ function setHeader(PoHeader $header) {
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoMetadataInterface.php b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoMetadataInterface.php
new file mode 100644
index 00000000..982aa392
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoMetadataInterface.php
@@ -0,0 +1,48 @@
+_langcode;
+ }
+
+ /**
+ * Implements PoMetadataInterface::setLangcode().
+ */
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ /**
+ * Implements PoMetadataInterface::getHeader().
+ */
+ public function getHeader() {
+ return $this->_header;
+ }
+
+ /**
+ * Implements PoMetadataInterface::setHeader().
+ *
+ * Not applicable to stream reading and therefore not implemented.
+ */
+ public function setHeader(PoHeader $header) {
+ }
+
+ /**
+ * Implements PoStreamInterface::getURI().
+ */
+ public function getURI() {
+ return $this->_uri;
+ }
+
+ /**
+ * Implements PoStreamInterface::setURI().
+ */
+ public function setURI($uri) {
+ $this->_uri = $uri;
+ }
+
+ /**
+ * Implements PoStreamInterface::open().
+ *
+ * Opens the stream and reads the header. The stream is ready for reading
+ * items after.
+ *
+ * @throws Exception
+ * If the URI is not yet set.
+ */
+ public function open() {
+ if (!empty($this->_uri)) {
+ $this->_fd = fopen($this->_uri, 'rb');
+ $this->_size = ftell($this->_fd);
+ $this->readHeader();
+ }
+ else {
+ throw new \Exception('Cannot open stream without URI set.');
+ }
+ }
+
+ /**
+ * Implements PoStreamInterface::close().
+ *
+ * @throws Exception
+ * If the stream is not open.
+ */
+ public function close() {
+ if ($this->_fd) {
+ fclose($this->_fd);
+ }
+ else {
+ throw new \Exception('Cannot close stream that is not open.');
+ }
+ }
+
+ /**
+ * Implements PoReaderInterface::readItem().
+ */
+ public function readItem() {
+ // Clear out the last item.
+ $this->_last_item = NULL;
+
+ // Read until finished with the stream or a complete item was identified.
+ while (!$this->_finished && is_null($this->_last_item)) {
+ $this->readLine();
+ }
+
+ return $this->_last_item;
+ }
+
+ /**
+ * Sets the seek position for the current PO stream.
+ *
+ * @param int $seek
+ * The new seek position to set.
+ */
+ public function setSeek($seek) {
+ fseek($this->_fd, $seek);
+ }
+
+ /**
+ * Returns the pointer position of the current PO stream.
+ */
+ public function getSeek() {
+ return ftell($this->_fd);
+ }
+
+ /**
+ * Read the header from the PO stream.
+ *
+ * The header is a special case PoItem, using the empty string as source and
+ * key-value pairs as translation. We just reuse the item reader logic to
+ * read the header.
+ */
+ private function readHeader() {
+ $item = $this->readItem();
+ // Handle the case properly when the .po file is empty (0 bytes).
+ if (!$item) {
+ return;
+ }
+ $header = new PoHeader;
+ $header->setFromString(trim($item->getTranslation()));
+ $this->_header = $header;
+ }
+
+ /**
+ * Reads a line from the PO stream and stores data internally.
+ *
+ * Expands $this->_current_item based on new data for the current item. If
+ * this line ends the current item, it is saved with setItemFromArray() with
+ * data from $this->_current_item.
+ *
+ * An internal state machine is maintained in this reader using $this->_context
+ * as the reading state. PO items are inbetween COMMENT states (when items have
+ * at least one line or comment inbetween them or indicated by MSGSTR or
+ * MSGSTR_ARR followed immediately by an MSGID or MSGCTXT (when items closely
+ * follow each other).
+ *
+ * @return
+ * FALSE if an error was logged, NULL otherwise. The errors are considered
+ * non-blocking, so reading can continue, while the errors are collected
+ * for later presentation.
+ */
+ private function readLine() {
+ // Read a line and set the stream finished indicator if it was not
+ // possible anymore.
+ $line = fgets($this->_fd);
+ $this->_finished = ($line === FALSE);
+
+ if (!$this->_finished) {
+
+ if ($this->_line_number == 0) {
+ // The first line might come with a UTF-8 BOM, which should be removed.
+ $line = str_replace("\xEF\xBB\xBF", '', $line);
+ // Current plurality for 'msgstr[]'.
+ $this->_current_plural_index = 0;
+ }
+
+ // Track the line number for error reporting.
+ $this->_line_number++;
+
+ // Initialize common values for error logging.
+ $log_vars = array(
+ '%uri' => $this->getURI(),
+ '%line' => $this->_line_number,
+ );
+
+ // Trim away the linefeed. \\n might appear at the end of the string if
+ // another line continuing the same string follows. We can remove that.
+ $line = trim(strtr($line, array("\\\n" => "")));
+
+ if (!strncmp('#', $line, 1)) {
+ // Lines starting with '#' are comments.
+
+ if ($this->_context == 'COMMENT') {
+ // Already in comment context, add to current comment.
+ $this->_current_item['#'][] = substr($line, 1);
+ }
+ elseif (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
+ // We are currently in string context, save current item.
+ $this->setItemFromArray($this->_current_item);
+
+ // Start a new entry for the comment.
+ $this->_current_item = array();
+ $this->_current_item['#'][] = substr($line, 1);
+
+ $this->_context = 'COMMENT';
+ return;
+ }
+ else {
+ // A comment following any other context is a syntax error.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars);
+ return FALSE;
+ }
+ return;
+ }
+ elseif (!strncmp('msgid_plural', $line, 12)) {
+ // A plural form for the current source string.
+
+ if ($this->_context != 'MSGID') {
+ // A plural form can only be added to an msgid directly.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Remove 'msgid_plural' and trim away whitespace.
+ $line = trim(substr($line, 12));
+
+ // Only the plural source string is left, parse it.
+ $quoted = $this->parseQuoted($line);
+ if ($quoted === FALSE) {
+ // The plural form must be wrapped in quotes.
+ $this->_errors[] = format_string('The translation stream %uri contains a syntax error on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Append the plural source to the current entry.
+ if (is_string($this->_current_item['msgid'])) {
+ // The first value was stored as string. Now we know the context is
+ // plural, it is converted to array.
+ $this->_current_item['msgid'] = array($this->_current_item['msgid']);
+ }
+ $this->_current_item['msgid'][] = $quoted;
+
+ $this->_context = 'MSGID_PLURAL';
+ return;
+ }
+ elseif (!strncmp('msgid', $line, 5)) {
+ // Starting a new message.
+
+ if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
+ // We are currently in string context, save current item.
+ $this->setItemFromArray($this->_current_item);
+
+ // Start a new context for the msgid.
+ $this->_current_item = array();
+ }
+ elseif ($this->_context == 'MSGID') {
+ // We are currently already in the context, meaning we passed an id with no data.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Remove 'msgid' and trim away whitespace.
+ $line = trim(substr($line, 5));
+
+ // Only the message id string is left, parse it.
+ $quoted = $this->parseQuoted($line);
+ if ($quoted === FALSE) {
+ // The message id must be wrapped in quotes.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars);
+ return FALSE;
+ }
+
+ $this->_current_item['msgid'] = $quoted;
+ $this->_context = 'MSGID';
+ return;
+ }
+ elseif (!strncmp('msgctxt', $line, 7)) {
+ // Starting a new context.
+
+ if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
+ // We are currently in string context, save current item.
+ $this->setItemFromArray($this->_current_item);
+ $this->_current_item = array();
+ }
+ elseif (!empty($this->_current_item['msgctxt'])) {
+ // A context cannot apply to another context.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Remove 'msgctxt' and trim away whitespaces.
+ $line = trim(substr($line, 7));
+
+ // Only the msgctxt string is left, parse it.
+ $quoted = $this->parseQuoted($line);
+ if ($quoted === FALSE) {
+ // The context string must be quoted.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ $this->_current_item['msgctxt'] = $quoted;
+
+ $this->_context = 'MSGCTXT';
+ return;
+ }
+ elseif (!strncmp('msgstr[', $line, 7)) {
+ // A message string for a specific plurality.
+
+ if (($this->_context != 'MSGID') &&
+ ($this->_context != 'MSGCTXT') &&
+ ($this->_context != 'MSGID_PLURAL') &&
+ ($this->_context != 'MSGSTR_ARR')) {
+ // Plural message strings must come after msgid, msgxtxt,
+ // msgid_plural, or other msgstr[] entries.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Ensure the plurality is terminated.
+ if (strpos($line, ']') === FALSE) {
+ $this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Extract the plurality.
+ $frombracket = strstr($line, '[');
+ $this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1);
+
+ // Skip to the next whitespace and trim away any further whitespace,
+ // bringing $line to the message text only.
+ $line = trim(strstr($line, " "));
+
+ $quoted = $this->parseQuoted($line);
+ if ($quoted === FALSE) {
+ // The string must be quoted.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
+ return FALSE;
+ }
+ if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) {
+ $this->_current_item['msgstr'] = array();
+ }
+
+ $this->_current_item['msgstr'][$this->_current_plural_index] = $quoted;
+
+ $this->_context = 'MSGSTR_ARR';
+ return;
+ }
+ elseif (!strncmp("msgstr", $line, 6)) {
+ // A string pair for an msgidid (with optional context).
+
+ if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT')) {
+ // Strings are only valid within an id or context scope.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Remove 'msgstr' and trim away away whitespaces.
+ $line = trim(substr($line, 6));
+
+ // Only the msgstr string is left, parse it.
+ $quoted = $this->parseQuoted($line);
+ if ($quoted === FALSE) {
+ // The string must be quoted.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ $this->_current_item['msgstr'] = $quoted;
+
+ $this->_context = 'MSGSTR';
+ return;
+ }
+ elseif ($line != '') {
+ // Anything that is not a token may be a continuation of a previous token.
+
+ $quoted = $this->parseQuoted($line);
+ if ($quoted === FALSE) {
+ // This string must be quoted.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars);
+ return FALSE;
+ }
+
+ // Append the string to the current item.
+ if (($this->_context == 'MSGID') || ($this->_context == 'MSGID_PLURAL')) {
+ if (is_array($this->_current_item['msgid'])) {
+ // Add string to last array element for plural sources.
+ $last_index = count($this->_current_item['msgid']) - 1;
+ $this->_current_item['msgid'][$last_index] .= $quoted;
+ }
+ else {
+ // Singular source, just append the string.
+ $this->_current_item['msgid'] .= $quoted;
+ }
+ }
+ elseif ($this->_context == 'MSGCTXT') {
+ // Multiline context name.
+ $this->_current_item['msgctxt'] .= $quoted;
+ }
+ elseif ($this->_context == 'MSGSTR') {
+ // Multiline translation string.
+ $this->_current_item['msgstr'] .= $quoted;
+ }
+ elseif ($this->_context == 'MSGSTR_ARR') {
+ // Multiline plural translation string.
+ $this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted;
+ }
+ else {
+ // No valid context to append to.
+ $this->_errors[] = format_string('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars);
+ return FALSE;
+ }
+ return;
+ }
+ }
+
+ // Empty line read or EOF of PO stream, close out the last entry.
+ if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
+ $this->setItemFromArray($this->_current_item);
+ $this->_current_item = array();
+ }
+ elseif ($this->_context != 'COMMENT') {
+ $this->_errors[] = format_string('The translation stream %uri ended unexpectedly at line %line.', $log_vars);
+ return FALSE;
+ }
+ }
+
+ /**
+ * Store the parsed values as a PoItem object.
+ */
+ public function setItemFromArray($value) {
+ $plural = FALSE;
+
+ $comments = '';
+ if (isset($value['#'])) {
+ $comments = $this->shortenComments($value['#']);
+ }
+
+ if (is_array($value['msgstr'])) {
+ // Sort plural variants by their form index.
+ ksort($value['msgstr']);
+ $plural = TRUE;
+ }
+
+ $item = new PoItem();
+ $item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : '');
+ $item->setSource($value['msgid']);
+ $item->setTranslation($value['msgstr']);
+ $item->setPlural($plural);
+ $item->setComment($comments);
+ $item->setLangcode($this->_langcode);
+
+ $this->_last_item = $item;
+
+ $this->_context = 'COMMENT';
+ }
+
+ /**
+ * Parses a string in quotes.
+ *
+ * @param $string
+ * A string specified with enclosing quotes.
+ *
+ * @return
+ * The string parsed from inside the quotes.
+ */
+ function parseQuoted($string) {
+ if (substr($string, 0, 1) != substr($string, -1, 1)) {
+ // Start and end quotes must be the same.
+ return FALSE;
+ }
+ $quote = substr($string, 0, 1);
+ $string = substr($string, 1, -1);
+ if ($quote == '"') {
+ // Double quotes: strip slashes.
+ return stripcslashes($string);
+ }
+ elseif ($quote == "'") {
+ // Simple quote: return as-is.
+ return $string;
+ }
+ else {
+ // Unrecognized quote.
+ return FALSE;
+ }
+ }
+
+ /**
+ * Generates a short, one-string version of the passed comment array.
+ *
+ * @param $comment
+ * An array of strings containing a comment.
+ *
+ * @return
+ * Short one-string version of the comment.
+ */
+ private function shortenComments($comment) {
+ $comm = '';
+ while (count($comment)) {
+ $test = $comm . substr(array_shift($comment), 1) . ', ';
+ if (strlen($comm) < 130) {
+ $comm = $test;
+ }
+ else {
+ break;
+ }
+ }
+ return trim(substr($comm, 0, -2));
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoStreamWriter.php b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoStreamWriter.php
new file mode 100644
index 00000000..cd316b90
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoStreamWriter.php
@@ -0,0 +1,160 @@
+_header;
+ }
+
+ /**
+ * Set the PO header for the current stream.
+ *
+ * @param PoHeader $header
+ * The Gettext PO header to set.
+ */
+ public function setHeader(PoHeader $header) {
+ $this->_header = $header;
+ }
+
+ /**
+ * Get the current language code used.
+ *
+ * @return string
+ * The language code.
+ */
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ /**
+ * Set the language code.
+ *
+ * @param string $langcode
+ * The language code.
+ */
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ /**
+ * Implements PoStreamInterface::open().
+ */
+ public function open() {
+ // Open in write mode. Will overwrite the stream if it already exists.
+ $this->_fd = fopen($this->getURI(), 'w');
+ // Write the header at the start.
+ $this->writeHeader();
+ }
+
+ /**
+ * Implements PoStreamInterface::close().
+ *
+ * @throws Exception
+ * If the stream is not open.
+ */
+ public function close() {
+ if ($this->_fd) {
+ fclose($this->_fd);
+ }
+ else {
+ throw new Exception('Cannot close stream that is not open.');
+ }
+ }
+
+ /**
+ * Write data to the stream.
+ *
+ * @param string $data
+ * Piece of string to write to the stream. If the value is not directly a
+ * string, casting will happen in writing.
+ *
+ * @throws Exception
+ * If writing the data is not possible.
+ */
+ private function write($data) {
+ $result = fputs($this->_fd, $data);
+ if ($result === FALSE) {
+ throw new Exception('Unable to write data: ' . substr($data, 0, 20));
+ }
+ }
+
+ /**
+ * Write the PO header to the stream.
+ */
+ private function writeHeader() {
+ $this->write($this->_header);
+ }
+
+ /**
+ * Implements PoWriterInterface::writeItem().
+ */
+ public function writeItem(PoItem $item) {
+ $this->write($item);
+ }
+
+ /**
+ * Implements PoWriterInterface::writeItems().
+ */
+ public function writeItems(PoReaderInterface $reader, $count = -1) {
+ $forever = $count == -1;
+ while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
+ $this->writeItem($item);
+ }
+ }
+
+ /**
+ * Implements PoStreamInterface::getURI().
+ *
+ * @throws Exception
+ * If the URI is not set.
+ */
+ public function getURI() {
+ if (empty($this->_uri)) {
+ throw new Exception('No URI set.');
+ }
+ return $this->_uri;
+ }
+
+ /**
+ * Implements PoStreamInterface::setURI().
+ */
+ public function setURI($uri) {
+ $this->_uri = $uri;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoWriterInterface.php b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoWriterInterface.php
new file mode 100644
index 00000000..cb486fee
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/gettext/PoWriterInterface.php
@@ -0,0 +1,32 @@
+ array(),
+ 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
+ 'items' => -1,
+ 'seek' => 0,
+ );
+ // Instantiate and initialize the stream reader for this file.
+ $reader = new PoStreamReader();
+ $reader->setLangcode($file->langcode);
+ $reader->setURI($file->uri);
+
+ try {
+ $reader->open();
+ }
+ catch (\Exception $exception) {
+ throw $exception;
+ }
+
+ $header = $reader->getHeader();
+ if (!$header) {
+ throw new \Exception('Missing or malformed header.');
+ }
+
+ // Initialize the database writer.
+ $writer = new PoDatabaseWriter();
+ $writer->setLangcode($file->langcode);
+ $writer_options = array(
+ 'overwrite_options' => $options['overwrite_options'],
+ 'customized' => $options['customized'],
+ );
+ $writer->setOptions($writer_options);
+ $writer->setHeader($header);
+
+ // Attempt to pipe all items from the file to the database.
+ try {
+ if ($options['seek']) {
+ $reader->setSeek($options['seek']);
+ }
+ $writer->writeItems($reader, $options['items']);
+ }
+ catch (\Exception $exception) {
+ throw $exception;
+ }
+
+ // Report back with an array of status information.
+ $report = $writer->getReport();
+
+ // Add the seek position to the report. This is useful for the batch
+ // operation.
+ $report['seek'] = $reader->getSeek();
+ return $report;
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/PoDatabaseReader.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/PoDatabaseReader.php
new file mode 100644
index 00000000..faddd372
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/PoDatabaseReader.php
@@ -0,0 +1,178 @@
+setOptions(array());
+ $this->storage = new StringDatabaseStorage();
+ }
+
+ /**
+ * Implements PoMetadataInterface::getLangcode().
+ */
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ /**
+ * Implements PoMetadataInterface::setLangcode().
+ */
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ /**
+ * Get the options used by the reader.
+ */
+ function getOptions() {
+ return $this->_options;
+ }
+
+ /**
+ * Set the options for the current reader.
+ */
+ function setOptions(array $options) {
+ $options += array(
+ 'customized' => FALSE,
+ 'not_customized' => FALSE,
+ 'not_translated' => FALSE,
+ );
+ $this->_options = $options;
+ }
+
+ /**
+ * Implements PoMetadataInterface::getHeader().
+ */
+ function getHeader() {
+ return new PoHeader($this->getLangcode());
+ }
+
+ /**
+ * Implements PoMetadataInterface::setHeader().
+ *
+ * @throws Exception
+ * Always, because you cannot set the PO header of a reader.
+ */
+ function setHeader(PoHeader $header) {
+ throw new \Exception('You cannot set the PO header in a reader.');
+ }
+
+ /**
+ * Builds and executes a database query based on options set earlier.
+ */
+ private function loadStrings() {
+ $langcode = $this->_langcode;
+ $options = $this->_options;
+ $conditions = array();
+
+ if (array_sum($options) == 0) {
+ // If user asked to not include anything in the translation files,
+ // that would not make sense, so just fall back on providing a template.
+ $langcode = NULL;
+ // Force option to get both translated and untranslated strings.
+ $options['not_translated'] = TRUE;
+ }
+ // Build and execute query to collect source strings and translations.
+ if (!empty($langcode)) {
+ $conditions['language'] = $langcode;
+ // Translate some options into field conditions.
+ if ($options['customized']) {
+ if (!$options['not_customized']) {
+ // Filter for customized strings only.
+ $conditions['customized'] = L10N_UPDATE_CUSTOMIZED;
+ }
+ // Else no filtering needed in this case.
+ }
+ else {
+ if ($options['not_customized']) {
+ // Filter for non-customized strings only.
+ $conditions['customized'] = L10N_UPDATE_NOT_CUSTOMIZED;
+ }
+ else {
+ // Filter for strings without translation.
+ $conditions['translated'] = FALSE;
+ }
+ }
+ if (!$options['not_translated']) {
+ // Filter for string with translation.
+ $conditions['translated'] = TRUE;
+ }
+ return $this->storage->getTranslations($conditions);
+ }
+ else {
+ // If no language, we don't need any of the target fields.
+ return $this->storage->getStrings($conditions);
+ }
+ }
+
+ /**
+ * Get the database result resource for the given language and options.
+ */
+ private function readString() {
+ if (!isset($this->_result)) {
+ $this->_result = $this->loadStrings();
+ }
+ return array_shift($this->_result);
+ }
+
+ /**
+ * Implements PoReaderInterface::readItem().
+ */
+ function readItem() {
+ if ($string = $this->readString()) {
+ $values = (array)$string;
+ $poItem = new PoItem();
+ $poItem->setFromArray($values);
+ return $poItem;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/PoDatabaseWriter.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/PoDatabaseWriter.php
new file mode 100644
index 00000000..50aaa330
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/PoDatabaseWriter.php
@@ -0,0 +1,296 @@
+setReport();
+ $this->storage = new StringDatabaseStorage();
+ }
+
+ /**
+ * Implements PoMetadataInterface::getLangcode().
+ */
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ /**
+ * Implements PoMetadataInterface::setLangcode().
+ */
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ /**
+ * Get the report of the write operations.
+ */
+ public function getReport() {
+ return $this->_report;
+ }
+
+ /**
+ * Set the report array of write operations.
+ *
+ * @param array $report
+ * Associative array with result information.
+ */
+ function setReport($report = array()) {
+ $report += array(
+ 'additions' => 0,
+ 'updates' => 0,
+ 'deletes' => 0,
+ 'skips' => 0,
+ 'strings' => array(),
+ );
+ $this->_report = $report;
+ }
+
+ /**
+ * Get the options used by the writer.
+ */
+ function getOptions() {
+ return $this->_options;
+ }
+
+ /**
+ * Set the options for the current writer.
+ */
+ function setOptions(array $options) {
+ if (!isset($options['overwrite_options'])) {
+ $options['overwrite_options'] = array();
+ }
+ $options['overwrite_options'] += array(
+ 'not_customized' => FALSE,
+ 'customized' => FALSE,
+ );
+ $options += array(
+ 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
+ );
+ $this->_options = $options;
+ }
+
+ /**
+ * Implements PoMetadataInterface::getHeader().
+ */
+ function getHeader() {
+ return $this->_header;
+ }
+
+ /**
+ * Implements PoMetadataInterface::setHeader().
+ *
+ * Sets the header and configure Drupal accordingly.
+ *
+ * Before being able to process the given header we need to know in what
+ * context this database write is done. For this the options must be set.
+ *
+ * A langcode is required to set the current header's PluralForm.
+ *
+ * @param PoHeader $header
+ * Header metadata.
+ *
+ * @throws Exception
+ */
+ function setHeader(PoHeader $header) {
+ $this->_header = $header;
+ $languages = language_list();
+
+ // Check for options.
+ $options = $this->getOptions();
+ if (empty($options)) {
+ throw new \Exception('Options should be set before assigning a PoHeader.');
+ }
+ $overwrite_options = $options['overwrite_options'];
+
+ // Check for langcode.
+ $langcode = $this->_langcode;
+ if (empty($langcode)) {
+ throw new \Exception('Langcode should be set before assigning a PoHeader.');
+ }
+
+ // Check is language is already created.
+ if (!isset($languages[$langcode])) {
+ throw new \Exception('Language should be known before using it.');
+ }
+
+ if (array_sum($overwrite_options) || empty($languages[$langcode]->plurals)) {
+ // Get and store the plural formula if available.
+ $plural = $header->getPluralForms();
+ if (isset($plural) && $p = $header->parsePluralForms($plural)) {
+ list($nplurals, $formula) = $p;
+ db_update('languages')
+ ->fields(array(
+ 'plurals' => $nplurals,
+ 'formula' => $formula,
+ ))
+ ->condition('language', $langcode)
+ ->execute();
+ }
+ }
+ }
+
+ /**
+ * Implements PoWriterInterface::writeItem().
+ */
+ function writeItem(PoItem $item) {
+ if ($item->isPlural()) {
+ $item->setSource(join(L10N_UPDATE_PLURAL_DELIMITER, $item->getSource()));
+ $item->setTranslation(join(L10N_UPDATE_PLURAL_DELIMITER, $item->getTranslation()));
+ }
+ $this->importString($item);
+ }
+
+ /**
+ * Implements PoWriterInterface::writeItems().
+ */
+ public function writeItems(PoReaderInterface $reader, $count = -1) {
+ $forever = $count == -1;
+ while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
+ $this->writeItem($item);
+ }
+ }
+
+ /**
+ * Imports one string into the database.
+ *
+ * @param PoItem $item
+ * The item being imported.
+ *
+ * @return int
+ * The string ID of the existing string modified or the new string added.
+ */
+ private function importString(PoItem $item) {
+ // Initialize overwrite options if not set.
+ $this->_options['overwrite_options'] += array(
+ 'not_customized' => FALSE,
+ 'customized' => FALSE,
+ );
+ $overwrite_options = $this->_options['overwrite_options'];
+ $customized = $this->_options['customized'];
+
+ $context = $item->getContext();
+ $source = $item->getSource();
+ $translation = $item->getTranslation();
+
+ // Look up the source string and any existing translation.
+ $strings = $this->storage->getTranslations(array(
+ 'language' => $this->_langcode,
+ 'source' => $source,
+ 'context' => $context
+ ));
+ $string = reset($strings);
+
+ if (!empty($translation)) {
+ // Skip this string unless it passes a check for dangerous code.
+ if (!locale_string_is_safe($translation)) {
+ watchdog('l10n_update', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR);
+ $this->_report['skips']++;
+ return 0;
+ }
+ elseif ($string) {
+ $string->setString($translation);
+ if ($string->isNew()) {
+ // No translation in this language.
+ $string->setValues(array(
+ 'language' => $this->_langcode,
+ 'customized' => $customized
+ ));
+ $string->save();
+ $this->_report['additions']++;
+ }
+ elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
+ // Translation exists, only overwrite if instructed.
+ $string->customized = $customized;
+ $string->save();
+ $this->_report['updates']++;
+ }
+ $this->_report['strings'][] = $string->getId();
+ return $string->lid;
+ }
+ else {
+ // No such source string in the database yet.
+ $string = $this->storage->createString(array('source' => $source, 'context' => $context))
+ ->save();
+ $target = $this->storage->createTranslation(array(
+ 'lid' => $string->getId(),
+ 'language' => $this->_langcode,
+ 'translation' => $translation,
+ 'customized' => $customized,
+ ))->save();
+
+ $this->_report['additions']++;
+ $this->_report['strings'][] = $string->getId();
+ return $string->lid;
+ }
+ }
+ elseif ($string && !$string->isNew() && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
+ // Empty translation, remove existing if instructed.
+ $string->delete();
+ $this->_report['deletes']++;
+ $this->_report['strings'][] = $string->lid;
+ return $string->lid;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/SourceString.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/SourceString.php
new file mode 100644
index 00000000..9f1815ed
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/SourceString.php
@@ -0,0 +1,52 @@
+source);
+ }
+
+ /**
+ * Implements StringInterface::isTranslation().
+ */
+ public function isTranslation() {
+ return FALSE;
+ }
+
+ /**
+ * Implements LocaleString::getString().
+ */
+ public function getString() {
+ return isset($this->source) ? $this->source : '';
+ }
+
+ /**
+ * Implements LocaleString::setString().
+ */
+ public function setString($string) {
+ $this->source = $string;
+ return $this;
+ }
+
+ /**
+ * Implements LocaleString::isNew().
+ */
+ public function isNew() {
+ return empty($this->lid);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringBase.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringBase.php
new file mode 100644
index 00000000..a623a4da
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringBase.php
@@ -0,0 +1,184 @@
+setValues((array)$values);
+ }
+
+ /**
+ * Implements StringInterface::getId().
+ */
+ public function getId() {
+ return isset($this->lid) ? $this->lid : NULL;
+ }
+
+ /**
+ * Implements StringInterface::setId().
+ */
+ public function setId($lid) {
+ $this->lid = $lid;
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::getVersion().
+ */
+ public function getVersion() {
+ return isset($this->version) ? $this->version : NULL;
+ }
+
+ /**
+ * Implements StringInterface::setVersion().
+ */
+ public function setVersion($version) {
+ $this->version = $version;
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::getPlurals().
+ */
+ public function getPlurals() {
+ return explode(L10N_UPDATE_PLURAL_DELIMITER, $this->getString());
+ }
+
+ /**
+ * Implements StringInterface::setPlurals().
+ */
+ public function setPlurals($plurals) {
+ $this->setString(implode(L10N_UPDATE_PLURAL_DELIMITER, $plurals));
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::getStorage().
+ */
+ public function getStorage() {
+ return isset($this->storage) ? $this->storage : NULL;
+ }
+
+ /**
+ * Implements StringInterface::setStorage().
+ */
+ public function setStorage($storage) {
+ $this->storage = $storage;
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::setValues().
+ */
+ public function setValues(array $values, $override = TRUE) {
+ foreach ($values as $key => $value) {
+ if (property_exists($this, $key) && ($override || !isset($this->$key))) {
+ $this->$key = $value;
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::getValues().
+ */
+ public function getValues(array $fields) {
+ $values = array();
+ foreach ($fields as $field) {
+ if (isset($this->$field)) {
+ $values[$field] = $this->$field;
+ }
+ }
+ return $values;
+ }
+
+ /**
+ * Implements LocaleString::save().
+ */
+ public function save() {
+ if ($storage = $this->getStorage()) {
+ $storage->save($this);
+ }
+ else {
+ throw new StringStorageException(format_string('The string cannot be saved because its not bound to a storage: @string', array(
+ '@string' => $this->getString()
+ )));
+ }
+ return $this;
+ }
+
+ /**
+ * Implements LocaleString::delete().
+ */
+ public function delete() {
+ if (!$this->isNew()) {
+ if ($storage = $this->getStorage()) {
+ $storage->delete($this);
+ }
+ else {
+ throw new StringStorageException(format_string('The string cannot be deleted because its not bound to a storage: @string', array(
+ '@string' => $this->getString()
+ )));
+ }
+ }
+ return $this;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringDatabaseStorage.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringDatabaseStorage.php
new file mode 100644
index 00000000..d478b07b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringDatabaseStorage.php
@@ -0,0 +1,518 @@
+options = $options;
+ }
+
+ /**
+ * Implements StringStorageInterface::getStrings().
+ */
+ public function getStrings(array $conditions = array(), array $options = array()) {
+ return $this->dbStringLoad($conditions, $options, 'SourceString');
+ }
+
+ /**
+ * Implements StringStorageInterface::getTranslations().
+ */
+ public function getTranslations(array $conditions = array(), array $options = array()) {
+ return $this->dbStringLoad($conditions, array('translation' => TRUE) + $options, 'TranslationString');
+ }
+
+ /**
+ * Implements StringStorageInterface::findString().
+ */
+ public function findString(array $conditions) {
+ $values = $this->dbStringSelect($conditions)
+ ->execute()
+ ->fetchAssoc();
+
+ if (!empty($values)) {
+ $string = new SourceString($values);
+ $string->setStorage($this);
+ return $string;
+ }
+ }
+
+ /**
+ * Implements StringStorageInterface::findTranslation().
+ */
+ public function findTranslation(array $conditions) {
+ $values = $this->dbStringSelect($conditions, array('translation' => TRUE))
+ ->execute()
+ ->fetchAssoc();
+
+ if (!empty($values)) {
+ $string = new TranslationString($values);
+ $this->checkVersion($string, VERSION);
+ $string->setStorage($this);
+ return $string;
+ }
+ }
+
+ /**
+ * Implements StringStorageInterface::countStrings().
+ */
+ public function countStrings() {
+ return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField();
+ }
+
+ /**
+ * Implements StringStorageInterface::countTranslations().
+ */
+ public function countTranslations() {
+ return $this->dbExecute("SELECT t.language, COUNT(*) AS translated FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY t.language")->fetchAllKeyed();
+ }
+
+ /**
+ * Implements StringStorageInterface::save().
+ */
+ public function save($string) {
+ if ($string->isNew()) {
+ $result = $this->dbStringInsert($string);
+ if ($string->isSource() && $result) {
+ // Only for source strings, we set the locale identifier.
+ $string->setId($result);
+ }
+ $string->setStorage($this);
+ }
+ else {
+ $this->dbStringUpdate($string);
+ }
+ return $this;
+ }
+
+ /**
+ * Checks whether the string version matches a given version, fix it if not.
+ *
+ * @param StringInterface $string
+ * The string object.
+ * @param string $version
+ * Drupal version to check against.
+ */
+ protected function checkVersion($string, $version) {
+ if ($string->getId() && $string->getVersion() != $version) {
+ $string->setVersion($version);
+ db_update('locales_source', $this->options)
+ ->condition('lid', $string->getId())
+ ->fields(array('version' => $version))
+ ->execute();
+ }
+ }
+
+ /**
+ * Implements StringStorageInterface::delete().
+ */
+ public function delete($string) {
+ if ($keys = $this->dbStringKeys($string)) {
+ $this->dbDelete('locales_target', $keys)->execute();
+ if ($string->isSource()) {
+ $this->dbDelete('locales_source', $keys)->execute();
+ $this->dbDelete('locales_location', $keys)->execute();
+ $string->setId(NULL);
+ }
+ }
+ else {
+ throw new StringStorageException(format_string('The string cannot be deleted because it lacks some key fields: @string', array(
+ '@string' => $string->getString()
+ )));
+ }
+ return $this;
+ }
+
+ /**
+ * Implements StringStorageInterface::deleteLanguage().
+ */
+ public function deleteStrings($conditions) {
+ $lids = $this->dbStringSelect($conditions, array('fields' => array('lid')))->execute()->fetchCol();
+ if ($lids) {
+ $this->dbDelete('locales_target', array('lid' => $lids))->execute();
+ $this->dbDelete('locales_source', array('lid' => $lids))->execute();
+ $this->dbDelete('locales_location', array('sid' => $lids))->execute();
+ }
+ }
+
+ /**
+ * Implements StringStorageInterface::deleteLanguage().
+ */
+ public function deleteTranslations($conditions) {
+ $this->dbDelete('locales_target', $conditions)->execute();
+ }
+
+ /**
+ * Implements StringStorageInterface::createString().
+ */
+ public function createString($values = array()) {
+ return new SourceString($values + array('storage' => $this));
+ }
+
+ /**
+ * Implements StringStorageInterface::createTranslation().
+ */
+ public function createTranslation($values = array()) {
+ return new TranslationString($values + array(
+ 'storage' => $this,
+ 'is_new' => TRUE
+ ));
+ }
+
+ /**
+ * Gets table alias for field.
+ *
+ * @param string $field
+ * Field name to find the table alias for.
+ *
+ * @return string
+ * Either 's', 't' or 'l' depending on whether the field belongs to source,
+ * target or location table table.
+ */
+ protected function dbFieldTable($field) {
+ if (in_array($field, array('language', 'translation', 'customized'))) {
+ return 't';
+ }
+ elseif (in_array($field, array('type', 'name'))) {
+ return 'l';
+ }
+ else {
+ return 's';
+ }
+ }
+
+ /**
+ * Gets table name for storing string object.
+ *
+ * @param StringInterface $string
+ * The string object.
+ *
+ * @return string
+ * The table name.
+ */
+ protected function dbStringTable($string) {
+ if ($string->isSource()) {
+ return 'locales_source';
+ }
+ elseif ($string->isTranslation()) {
+ return 'locales_target';
+ }
+ }
+
+ /**
+ * Gets keys values that are in a database table.
+ *
+ * @param StringInterface $string
+ * The string object.
+ *
+ * @return array
+ * Array with key fields if the string has all keys, or empty array if not.
+ */
+ protected function dbStringKeys($string) {
+ if ($string->isSource()) {
+ $keys = array('lid');
+ }
+ elseif ($string->isTranslation()) {
+ $keys = array('lid', 'language');
+ }
+ if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
+ return $values;
+ }
+ else {
+ return array();
+ }
+ }
+
+ /**
+ * Loads multiple string objects.
+ *
+ * @param array $conditions
+ * Any of the conditions used by dbStringSelect().
+ * @param array $options
+ * Any of the options used by dbStringSelect().
+ * @param string $class
+ * Class name to use for fetching returned objects.
+ *
+ * @return array
+ * Array of objects of the class requested.
+ */
+ protected function dbStringLoad(array $conditions, array $options, $class) {
+ $strings = array();
+ $result = $this->dbStringSelect($conditions, $options)->execute();
+ foreach ($result as $item) {
+ $string = new $class($item);
+ $string->setStorage($this);
+ $strings[] = $string;
+ }
+ return $strings;
+ }
+
+ /**
+ * Builds a SELECT query with multiple conditions and fields.
+ *
+ * The query uses both 'locales_source' and 'locales_target' tables.
+ * Note that by default, as we are selecting both translated and untranslated
+ * strings target field's conditions will be modified to match NULL rows too.
+ *
+ * @param array $conditions
+ * An associative array with field => value conditions that may include
+ * NULL values. If a language condition is included it will be used for
+ * joining the 'locales_target' table.
+ * @param array $options
+ * An associative array of additional options. It may contain any of the
+ * options used by StringStorageInterface::getStrings() and these additional
+ * ones:
+ * - 'translation', Whether to include translation fields too. Defaults to
+ * FALSE.
+ * @return SelectQuery
+ * Query object with all the tables, fields and conditions.
+ */
+ protected function dbStringSelect(array $conditions, array $options = array()) {
+ // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
+ // backported code to work with the Drupal 7 style database tables.
+ if (isset($conditions['customized'])) {
+ $conditions['l10n_status'] = $conditions['customized'];
+ unset($conditions['customized']);
+ }
+ if (isset($options['customized'])) {
+ $options['l10n_status'] = $options['customized'];
+ unset($options['customized']);
+ }
+ // Start building the query with source table and check whether we need to
+ // join the target table too.
+ $query = db_select('locales_source', 's', $this->options)
+ ->fields('s');
+
+ // Figure out how to join and translate some options into conditions.
+ if (isset($conditions['translated'])) {
+ // This is a meta-condition we need to translate into simple ones.
+ if ($conditions['translated']) {
+ // Select only translated strings.
+ $join = 'innerJoin';
+ }
+ else {
+ // Select only untranslated strings.
+ $join = 'leftJoin';
+ $conditions['translation'] = NULL;
+ }
+ unset($conditions['translated']);
+ }
+ else {
+ $join = !empty($options['translation']) ? 'leftJoin' : FALSE;
+ }
+
+ if ($join) {
+ if (isset($conditions['language'])) {
+ // If we've got a language condition, we use it for the join.
+ $query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(
+ ':langcode' => $conditions['language']
+ ));
+ unset($conditions['language']);
+ }
+ else {
+ // Since we don't have a language, join with locale id only.
+ $query->$join('locales_target', 't', "t.lid = s.lid");
+ }
+ if (!empty($options['translation'])) {
+ // We cannot just add all fields because 'lid' may get null values.
+ $query->addField('t', 'language');
+ $query->addField('t', 'translation');
+ $query->addField('t', 'l10n_status', 'customized');
+ }
+ }
+
+ // If we have conditions for location's type or name, then we need the
+ // location table, for which we add a subquery.
+ if (isset($conditions['type']) || isset($conditions['name'])) {
+ $subquery = db_select('locales_location', 'l', $this->options)
+ ->fields('l', array('sid'));
+ foreach (array('type', 'name') as $field) {
+ if (isset($conditions[$field])) {
+ $subquery->condition('l.' . $field, $conditions[$field]);
+ unset($conditions[$field]);
+ }
+ }
+ $query->condition('s.lid', $subquery, 'IN');
+ }
+
+ // Add conditions for both tables.
+ foreach ($conditions as $field => $value) {
+ $table_alias = $this->dbFieldTable($field);
+ $field_alias = $table_alias . '.' . $field;
+ if (is_null($value)) {
+ $query->isNull($field_alias);
+ }
+ elseif ($table_alias == 't' && $join === 'leftJoin') {
+ // Conditions for target fields when doing an outer join only make
+ // sense if we add also OR field IS NULL.
+ $query->condition(db_or()
+ ->condition($field_alias, $value)
+ ->isNull($field_alias)
+ );
+ }
+ else {
+ $query->condition($field_alias, $value);
+ }
+ }
+
+ // Process other options, string filter, query limit, etc...
+ if (!empty($options['filters'])) {
+ if (count($options['filters']) > 1) {
+ $filter = db_or();
+ $query->condition($filter);
+ }
+ else {
+ // If we have a single filter, just add it to the query.
+ $filter = $query;
+ }
+ foreach ($options['filters'] as $field => $string) {
+ $filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE');
+ }
+ }
+
+ if (!empty($options['pager limit'])) {
+ $query = $query->extend('PagerDefault')->limit($options['pager limit']);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Createds a database record for a string object.
+ *
+ * @param StringInterface $string
+ * The string object.
+ *
+ * @return bool|int
+ * If the operation failed, returns FALSE.
+ * If it succeeded returns the last insert ID of the query, if one exists.
+ *
+ * @throws StringStorageException
+ * If the string is not suitable for this storage, an exception ithrown.
+ */
+ protected function dbStringInsert($string) {
+ if ($string->isSource()) {
+ $string->setValues(array('context' => '', 'version' => 'none'), FALSE);
+ $fields = $string->getValues(array('source', 'context', 'version'));
+ }
+ elseif ($string->isTranslation()) {
+ $string->setValues(array('customized' => 0), FALSE);
+ $fields = $string->getValues(array('lid', 'language', 'translation', 'customized'));
+ }
+ if (!empty($fields)) {
+ // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
+ // backported code to work with the Drupal 7 style database tables.
+ if (isset($fields['customized'])) {
+ $fields['l10n_status'] = $fields['customized'];
+ unset($fields['customized']);
+ }
+
+ return db_insert($this->dbStringTable($string), $this->options)
+ ->fields($fields)
+ ->execute();
+ }
+ else {
+ throw new StringStorageException(format_string('The string cannot be saved: @string', array(
+ '@string' => $string->getString()
+ )));
+ }
+ }
+
+ /**
+ * Updates string object in the database.
+ *
+ * @param StringInterface $string
+ * The string object.
+ *
+ * @return bool|int
+ * If the record update failed, returns FALSE. If it succeeded, returns
+ * SAVED_NEW or SAVED_UPDATED.
+ *
+ * @throws StringStorageException
+ * If the string is not suitable for this storage, an exception is thrown.
+ */
+ protected function dbStringUpdate($string) {
+ if ($string->isSource()) {
+ $values = $string->getValues(array('source', 'context', 'version'));
+ }
+ elseif ($string->isTranslation()) {
+ $values = $string->getValues(array('translation', 'customized'));
+ }
+ if (!empty($values) && $keys = $this->dbStringKeys($string)) {
+ // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
+ // backported code to work with the Drupal 7 style database tables.
+ if (isset($keys['customized'])) {
+ $keys['l10n_status'] = $keys['customized'];
+ unset($keys['customized']);
+ }
+ if (isset($values['customized'])) {
+ $values['l10n_status'] = $values['customized'];
+ unset($values['customized']);
+ }
+
+ return db_merge($this->dbStringTable($string), $this->options)
+ ->key($keys)
+ ->fields($values)
+ ->execute();
+ }
+ else {
+ throw new StringStorageException(format_string('The string cannot be updated: @string', array(
+ '@string' => $string->getString()
+ )));
+ }
+ }
+
+ /**
+ * Creates delete query.
+ *
+ * @param string $table
+ * The table name.
+ * @param array $keys
+ * Array with object keys indexed by field name.
+ *
+ * @return DeleteQuery
+ * Returns a new DeleteQuery object for the active database.
+ */
+ protected function dbDelete($table, $keys) {
+ $query = db_delete($table, $this->options);
+ // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
+ // backported code to work with the Drupal 7 style database tables.
+ if (isset($keys['customized'])) {
+ $keys['l10n_status'] = $keys['customized'];
+ unset($keys['customized']);
+ }
+
+ foreach ($keys as $field => $value) {
+ $query->condition($field, $value);
+ }
+ return $query;
+ }
+
+ /**
+ * Executes an arbitrary SELECT query string.
+ */
+ protected function dbExecute($query, array $args = array()) {
+ return db_query($query, $args, $this->options);
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringInterface.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringInterface.php
new file mode 100644
index 00000000..9709c821
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/StringInterface.php
@@ -0,0 +1,180 @@
+is_new)) {
+ // We mark the string as not new if it is a complete translation.
+ // This will work when loading from database, otherwise the storage
+ // controller that creates the string object must handle it.
+ $this->is_new = !$this->isTranslation();
+ }
+ }
+
+ /**
+ * Sets the string as customized / not customized.
+ *
+ * @param bool $customized
+ * (optional) Whether the string is customized or not. Defaults to TRUE.
+ *
+ * @return TranslationString
+ * The called object.
+ */
+ public function setCustomized($customized = TRUE) {
+ $this->customized = $customized ? L10N_UPDATE_CUSTOMIZED : L10N_UPDATE_NOT_CUSTOMIZED;
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::isSource().
+ */
+ public function isSource() {
+ return FALSE;
+ }
+
+ /**
+ * Implements StringInterface::isTranslation().
+ */
+ public function isTranslation() {
+ return !empty($this->lid) && !empty($this->language) && isset($this->translation);
+ }
+
+ /**
+ * Implements StringInterface::getString().
+ */
+ public function getString() {
+ return isset($this->translation) ? $this->translation : '';
+ }
+
+ /**
+ * Implements StringInterface::setString().
+ */
+ public function setString($string) {
+ $this->translation = $string;
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::isNew().
+ */
+ public function isNew() {
+ return $this->is_new;
+ }
+
+ /**
+ * Implements StringInterface::save().
+ */
+ public function save() {
+ parent::save();
+ $this->is_new = FALSE;
+ return $this;
+ }
+
+ /**
+ * Implements StringInterface::delete().
+ */
+ public function delete() {
+ parent::delete();
+ $this->is_new = TRUE;
+ return $this;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/includes/locale/TranslationsStreamWrapper.php b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/TranslationsStreamWrapper.php
new file mode 100644
index 00000000..1b1948a1
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/includes/locale/TranslationsStreamWrapper.php
@@ -0,0 +1,27 @@
+')
- .prepend($legend.contents())
- .appendTo($legend)
- .click(function () {
- Drupal.toggleFieldset(wrapper);
- return false;
- });
- });
- }
-};
-
-})(jQuery);
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update-translation-last-check.tpl.php b/sites/all/modules/contrib/localisation/l10n_update/l10n_update-translation-last-check.tpl.php
new file mode 100644
index 00000000..a6798188
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update-translation-last-check.tpl.php
@@ -0,0 +1,18 @@
+
+
+ ()
+
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update-translation-update-info.tpl.php b/sites/all/modules/contrib/localisation/l10n_update/l10n_update-translation-update-info.tpl.php
new file mode 100644
index 00000000..3a560625
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update-translation-update-info.tpl.php
@@ -0,0 +1,31 @@
+
+
+ Show description
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.admin.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.admin.inc
index bebee3ee..96dae0f7 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.admin.inc
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.admin.inc
@@ -6,163 +6,205 @@
*/
/**
- * Project has a new release available.
+ * Page callback: Checks for translation updates and displays the status.
+ *
+ * Manually checks the translation status without the use of cron.
*/
-define('L10N_UPDATE_NOT_CURRENT', 4);
+function l10n_update_manual_status() {
+ module_load_include('compare.inc', 'l10n_update');
-/**
- * Project is up to date.
- */
-define('L10N_UPDATE_CURRENT', 5);
+ // Check the translation status of all translatable projects in all languages.
+ // First we clear the cached list of projects. Although not strictly
+ // necessary, this is helpful in case the project list is out of sync.
+ l10n_update_flush_projects();
+ l10n_update_check_projects();
-/**
- * Project's status cannot be checked.
- */
-define('L10N_UPDATE_NOT_CHECKED', -1);
-
-/**
- * No available update data was found for project.
- */
-define('L10N_UPDATE_UNKNOWN', -2);
-
-/**
- * There was a failure fetching available update data for this project.
- */
-define('L10N_UPDATE_NOT_FETCHED', -3);
-
-// Include l10n_update API
-module_load_include('check.inc', 'l10n_update');
-// And project api
-module_load_include('project.inc', 'l10n_update');
-
-/**
- * Page callback: Admin overview page.
- */
-function l10n_update_admin_overview() {
- // For now we get package information provided by modules.
- $projects = l10n_update_get_projects();
- $languages = l10n_update_language_list('name');
-
- $build = array();
- if ($languages) {
- $history = l10n_update_get_history();
- $available = l10n_update_available_releases();
- $updates = l10n_update_build_updates($history, $available);
- $build['project_status'] = array(
- '#theme' => 'l10n_update_project_status',
- '#projects' => $projects,
- '#languages' => $languages,
- '#history' => $history,
- '#available' => $available,
- '#updates' => $updates,
- );
- $build['admin_import_form'] = drupal_get_form('l10n_update_admin_import_form', $projects, $updates);
+ // Execute a batch if required. A batch is only used when remote files
+ // are checked.
+ if (batch_get()) {
+ batch_process('admin/config/regional/translate/update');
}
- else {
- $build['no_projects'] = array('#markup' => t('No projects or languages to update.'));
- }
- return $build;
+ drupal_goto('admin/config/regional/translate/update');
}
/**
- * Translation update form.
- *
- * @todo selectable packages
- * @todo check language support in server
- * @todo check file update dates
- *
- * @param $form_state
- * Form states array.
- * @param $projects
- * @todo $projects are not used in the form.
- * @param $updates
- * Updates to be displayed in the form.
+ * Page callback: Translation status page.
*/
-function l10n_update_admin_import_form($form, $form_state, $projects, $updates) {
- //module_load_include('inc', 'l10n_update');
- // For now we get package information provided by modules
- $projects = l10n_update_get_projects();
- $languages = l10n_update_language_list('name');
+function l10n_update_status_form() {
+ module_load_include('compare.inc', 'l10n_update');
+ $updates = $options = array();
+ $languages_update = $languages_not_found = array();
+ $projects_update = array();
- // Absence of projects is an error and only occurs if the database table
- // was truncated. In this case we rebuild the project data.
- if (!$projects) {
- l10n_update_build_projects();
- $projects = l10n_update_get_projects();
+ // @todo Calling l10n_update_build_projects() is an expensive way to
+ // get a module name. In follow-up issue http://drupal.org/node/1842362
+ // the project name will be stored to display use, like here.
+ $project_data = l10n_update_build_projects();
+ $languages = l10n_update_translatable_language_list();
+ $status = l10n_update_get_status();
+
+ // Prepare information about projects which have available translation
+ // updates.
+ if ($languages && $status) {
+ foreach ($status as $project) {
+ foreach ($project as $langcode => $project_info) {
+ if (isset($project_data[$project_info->name])) {
+ // No translation file found for this project-language combination.
+ if (empty($project_info->type)) {
+ $updates[$langcode]['not_found'][] = array(
+ 'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'],
+ 'version' => $project_info->version,
+ 'info' => _l10n_update_status_debug_info($project_info),
+ );
+ $languages_not_found[$langcode] = $langcode;
+ }
+ // Translation update found for this project-language combination.
+ elseif ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE ) {
+ $local = isset($project_info->files[L10N_UPDATE_LOCAL]) ? $project_info->files[L10N_UPDATE_LOCAL] : NULL;
+ $remote = isset($project_info->files[L10N_UPDATE_REMOTE]) ? $project_info->files[L10N_UPDATE_REMOTE] : NULL;
+ $recent = _l10n_update_source_compare($local, $remote) == L10N_UPDATE_SOURCE_COMPARE_LT ? $remote : $local;
+ $updates[$langcode]['updates'][] = array(
+ 'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'],
+ 'version' => $project_info->version,
+ 'timestamp' => $recent->timestamp,
+ );
+ $languages_update[$langcode] = $langcode;
+ $projects_update[$project_info->name] = $project_info->name;
+ }
+ }
+ }
+ }
+ $languages_not_found = array_diff($languages_not_found, $languages_update);
+
+ // Build data options for the select table.
+ foreach($updates as $langcode => $update) {
+ $title = check_plain($languages[$langcode]);
+ $l10n_update_update_info = array('#theme' => 'l10n_update_update_info');
+ foreach (array('updates', 'not_found') as $update_status) {
+ if (isset($update[$update_status])) {
+ $l10n_update_update_info['#' . $update_status] = $update[$update_status];
+ }
+ }
+ $options[$langcode] = array(
+ 'title' => array(
+ 'class' => array('label'),
+ 'data' => array(
+ '#title' => $title,
+ '#markup' => $title
+ ),
+ ),
+ 'status' => array('class' => array('description', 'expand', 'priority-low'), 'data' => drupal_render($l10n_update_update_info)),
+ );
+ }
+ // Sort the table data on language name.
+ uasort($options, function ($a, $b) {
+ return strcasecmp($a['title']['data']['#title'], $b['title']['data']['#title']);
+ });
}
- if ($projects && $languages) {
- $form['updates'] = array(
- '#type' => 'value',
- '#value' => $updates,
- );
- // @todo Only show this language fieldset if we have more than 1 language.
- $form['lang'] = array(
- '#type' => 'fieldset',
- '#title' => t('Languages'),
- '#collapsible' => TRUE,
- '#collapsed' => TRUE,
- '#description' => t('Select one or more languages to download and update. If you select none, all of them will be updated.'),
- );
- $form['lang']['languages'] = array(
- '#type' => 'checkboxes',
- '#options' => $languages,
- );
- $form['mode'] = array(
- '#type' => 'radios',
- '#title' => t('Update mode'),
- '#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
- '#options' => _l10n_update_admin_import_options(),
- );
- $form['buttons']['download'] = array(
- '#type' => 'submit',
- '#value' => t('Update translations'),
- );
- }
- $form['buttons']['refresh'] = array(
- '#type' => 'submit',
- '#value' => t('Refresh information'),
+ $last_checked = variable_get('l10n_update_last_check');
+ $form['last_checked'] = array(
+ '#theme' => 'l10n_update_last_check',
+ '#last' => $last_checked,
);
+
+ $header = array(
+ 'title' => array(
+ 'data' => t('Language'),
+ 'class' => array('title'),
+ ),
+ 'status' => array(
+ 'data' => t('Status'),
+ 'class' => array('status', 'priority-low'),
+ ),
+ );
+
+ if (!$languages) {
+ $empty = t('No translatable languages available. Add a language first.', array('@add_language' => url('admin/config/regional/language')));
+ }
+ elseif (empty($options)) {
+ $empty = t('All translations up to date.');
+ }
+ else {
+ $empty = t('No translation status available. Check manually.', array('@check' => url('admin/config/regional/translate/check')));
+ }
+
+ // The projects which require an update. Used by the _submit callback.
+ $form['projects_update'] = array(
+ '#type' => 'value',
+ '#value' => $projects_update,
+ );
+ $form['langcodes'] = array(
+ '#type' => 'tableselect',
+ '#header' => $header,
+ '#options' => $options,
+ '#default_value' => $languages_update,
+ '#empty' => $empty,
+ '#js_select' => TRUE,
+ '#multiple' => TRUE,
+ '#required' => TRUE,
+ '#not_found' => $languages_not_found,
+ '#after_build' => array('l10n_update_language_table'),
+ '#attributes' => array(),
+ );
+
+ $form['#attached'] = array(
+ 'js' => array(
+ drupal_get_path('module', 'l10n_update') . '/js/l10n_update.admin.js',
+ ),
+ 'css' => array(
+ drupal_get_path('module', 'l10n_update') . '/css/l10n_update.admin.css',
+ ),
+ );
+
+ if ($languages_update) {
+ $form['actions'] = array(
+ '#type' => 'actions',
+ 'submit' => array(
+ '#type' => 'submit',
+ '#value' => t('Update translations'),
+ ),
+ '#attributes' => array(),
+ );
+ }
+
return $form;
}
/**
- * Submit handler for Update form.
- *
- * Handles both submit buttons to update translations and to update the
- * form information.
+ * Form validation handler for locale_translation_status_form().
*/
-function l10n_update_admin_import_form_submit($form, $form_state) {
- $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
- $projects = l10n_update_get_projects();
-
- if ($op == t('Update translations')) {
- $languages = array_filter($form_state['values']['languages']);
- $updates = $form_state['values']['updates'];
- $mode = $form_state['values']['mode'];
- if ($projects && $updates) {
- module_load_include('batch.inc', 'l10n_update');
- // Filter out updates in other languages. If no languages, all of them will be updated
- $updates = _l10n_update_prepare_updates($updates, NULL, $languages);
- $batch = l10n_update_batch_multiple($updates, $mode);
- batch_set($batch);
- }
- else {
- drupal_set_message(t('Cannot find any translation updates.'), 'error');
- }
+function l10n_update_status_form_validate($form, &$form_state) {
+ // Check if a language has been selected. 'tableselect' doesn't.
+ if (!array_filter($form_state['values']['langcodes'])) {
+ form_set_error('', t('Select a language to update.'));
}
- elseif ($op == t('Refresh information')) {
- // Get current version of projects.
- l10n_update_build_projects();
+}
- // Get available translation updates and update file history.
- if ($available = l10n_update_available_releases(TRUE)) {
- l10n_update_flag_history($available);
- drupal_set_message(t('Fetched information about available updates from the server'));
- }
- else {
- drupal_set_message(t('Failed to fetch information about available updates from the server.'), 'error');
- }
+/**
+ * Form submission handler for locale_translation_status_form().
+ */
+function l10n_update_status_form_submit($form, $form_state) {
+ module_load_include('fetch.inc', 'l10n_update');
+ $langcodes = array_filter($form_state['values']['langcodes']);
+ $projects = array_filter($form_state['values']['projects_update']);
+
+ // Set the translation import options. This determines if existing
+ // translations will be overwritten by imported strings.
+ $options = _l10n_update_default_update_options();
+
+ // If the status was updated recently we can immediately start fetching the
+ // translation updates. If the status is expired we clear it an run a batch to
+ // update the status and then fetch the translation updates.
+ $last_checked = variable_get('l10n_update_last_check');
+ if ($last_checked < REQUEST_TIME - L10N_UPDATE_STATUS_TTL) {
+ l10n_update_clear_status();
+ $batch = l10n_update_batch_update_build(array(), $langcodes, $options);
+ batch_set($batch);
+ }
+ else {
+ $batch = l10n_update_batch_fetch_build($projects, $langcodes, $options);
+ batch_set($batch);
}
}
@@ -170,56 +212,80 @@ function l10n_update_admin_import_form_submit($form, $form_state) {
* Page callback: Settings form.
*/
function l10n_update_admin_settings_form($form, &$form_state) {
- $form['l10n_update_check_mode'] = array(
- '#type' => 'radios',
- '#title' => t('Update source'),
- '#default_value' => variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL),
- '#options' => _l10n_update_admin_check_options(),
- );
- $form['l10n_update_import_mode'] = array(
- '#type' => 'radios',
- '#title' => t('Update mode'),
- '#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
- '#options' => _l10n_update_admin_import_options(),
- );
$form['l10n_update_check_frequency'] = array(
'#type' => 'radios',
'#title' => t('Check for updates'),
- '#default_value' => variable_get('l10n_update_check_frequency', 0),
+ '#default_value' => variable_get('l10n_update_check_frequency', '0'),
'#options' => array(
- 0 => t('Never (manually)'),
- 1 => t('Daily'),
- 7 => t('Weekly'),
+ '0' => t('Never (manually)'),
+ '7' => t('Weekly'),
+ '30' => t('Monthly'),
),
- '#description' => t('Select how frequently you want to automatically check for updated translations for installed modules and themes.'),
+ '#description' => t('Select how frequently you want to check for new interface translations for your currently installed modules and themes. Check updates now.', array('@url' => url('admin/config/regional/translate/check'))),
);
+
$form['l10n_update_check_disabled'] = array(
'#type' => 'checkbox',
'#title' => t('Check for updates of disabled modules and themes'),
- '#default_value' => variable_get('l10n_update_check_disabled', 0),
- '#description' => t('Note that this comes with a performance penalty, so it is not recommended.'),
+ '#default_value' => variable_get('l10n_update_check_disabled', FALSE),
);
+
+ $form['l10n_update_check_mode'] = array(
+ '#type' => 'radios',
+ '#title' => t('Translation source'),
+ '#default_value' => variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL),
+ '#options' => array(
+ L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL => t('Drupal translation server and local files'),
+ L10N_UPDATE_USE_SOURCE_LOCAL => t('Local files only'),
+ ),
+ '#description' => t('The source of translation files for automatic interface translation.'),
+ );
+
$form['l10n_update_download_store'] = array(
- '#title' => t('Store downloaded files'),
+ '#title' => t('Translations directory'),
'#type' => 'textfield',
- '#default_value' => variable_get('l10n_update_download_store', ''),
- '#description' => t('A path relative to the Drupal installation directory where translation files will be stored, e.g. sites/all/translations. Saved translation files can be reused by other installations. If left empty the downloaded translation will not be saved.'),
+ '#default_value' => variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
+ '#required' => TRUE,
+ '#description' => t('A path relative to the Drupal installation directory where translation files will be stored, e.g. sites/all/translations. Saved translation files can be reused by other installations.'),
);
+
+ $form['l10n_update_import_mode'] = array(
+ '#type' => 'radios',
+ '#title' => t('Import behaviour'),
+ '#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
+ '#options' => array(
+ LOCALE_IMPORT_KEEP => t("Don't overwrite existing translations."),
+ L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED => t('Only overwrite imported translations, customized translations are kept.'),
+ LOCALE_IMPORT_OVERWRITE => t('Overwrite existing translations.'),
+ ),
+ '#description' => t('How to treat existing translations when automatically updating the interface translations.'),
+ );
+
+ $form['#submit'][] = 'l10n_update_admin_settings_form_submit';
return system_settings_form($form);
}
/**
- * Additional validation handler for update settings.
- *
- * Check for existing files directory and creates one when required.
+ * Validation handler for translation update settings.
*/
function l10n_update_admin_settings_form_validate($form, &$form_state) {
- $form_values = $form_state['values'];
- if (!empty($form_values['l10n_update_download_store'])) {
- if (!file_prepare_directory($form_values['l10n_update_download_store'], FILE_CREATE_DIRECTORY, 'l10n_update_download_store')) {
- form_set_error('l10n_update_download_store', t('The directory %directory does not exist or is not writable.', array('%directory' => $form_values['l10n_update_download_store'])));
- watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $form_values['l10n_update_download_store']), WATCHDOG_ERROR);
- }
+ // Check for existing translations directory and create one if required.
+ $directory = $form_state['values']['l10n_update_download_store'];
+ if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+ form_set_error('l10n_update_download_store', t('The directory %directory does not exist or is not writable.', array('%directory' => $directory)));
+ watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $directory), WATCHDOG_ERROR);
+ }
+}
+
+/**
+ * Submit handler for translation update settings.
+ */
+function l10n_update_admin_settings_form_submit($form, $form_state) {
+ // Invalidate the cached translation status when the configuration setting of
+ // 'l10n_update_check_mode' or 'check_disabled' change.
+ if ($form['l10n_update_check_mode']['#default_value'] != $form_state['values']['l10n_update_check_mode'] ||
+ $form['l10n_update_check_disabled']['#default_value'] != $form_state['values']['l10n_update_check_disabled']) {
+ l10n_update_clear_status();
}
}
@@ -235,316 +301,190 @@ function l10n_update_admin_settings_form_validate($form, &$form_state) {
function _l10n_update_admin_import_options() {
return array(
LOCALE_IMPORT_OVERWRITE => t('Translation updates replace existing ones, new ones are added'),
- LOCALE_UPDATE_OVERRIDE_DEFAULT => t('Edited translations are kept, only previously imported ones are overwritten and new translations are added'),
+ L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED => t('Edited translations are kept, only previously imported ones are overwritten and new translations are added'),
LOCALE_IMPORT_KEEP => t('All existing translations are kept, only new translations are added.'),
);
}
/**
- * Get array of check options.
+ * Provides debug info for projects in case translation files are not found.
*
- * @return
- * Keyed array of source download options.
- */
-function _l10n_update_admin_check_options() {
- return array(
- L10N_UPDATE_CHECK_ALL => t('Local files and remote server.'),
- L10N_UPDATE_CHECK_LOCAL => t('Local files only.'),
- L10N_UPDATE_CHECK_REMOTE => t('Remote server only.'),
- );
-}
-
-/**
- * Format project update status.
+ * Translations files are being fetched either from Drupal translation server
+ * and local files or only from the local filesystem depending on the
+ * "Translation source" setting at admin/config/regional/language/update.
+ * This method will produce debug information including the respective path(s)
+ * based on this setting.
*
- * @param $variables
- * An associative array containing:
- * - projects: An array containing all enabled projects.
- * - languages: An array of all enabled languages.
- * - history: An array of the current translations per project.
- * - available: An array of translation sources per project.
- * - updates: An array of available translation updates per project.
- * Only recommended translations are listed.
+ * Translations for development versions are never fetched, so the debug info
+ * for that is a fixed message.
+ *
+ * @param array $source
+ * An array which is the project information of the source.
*
* @return string
- * HTML output.
+ * The string which contains debug information.
*/
-function theme_l10n_update_project_status($variables) {
- $header = $rows = array();
+function _l10n_update_status_debug_info($source) {
+ $remote_path = isset($source->files['remote']->uri) ? $source->files['remote']->uri : '';
+ $local_path = isset($source->files['local']->uri) ? $source->files['local']->uri : '';
- // Get module and theme data for the project title.
- $projects = system_rebuild_module_data();
- $projects += system_rebuild_theme_data();
-
- foreach ($variables['projects'] as $name => $project) {
- if (isset($variables['history'][$name])) {
- if (isset($variables['updates'][$name])) {
- $project_status = 'updatable';
- $project_class = 'warning';
- }
- else {
- $project_status = 'uptodate';
- $project_class = 'ok';
- }
- }
- elseif (isset($variables['available'][$name])) {
- $project_status = 'available';
- $project_class = 'warning';
- }
- else {
- // Remote information not checked
- $project_status = 'unknown';
- $project_class = 'unknown';
- }
-
- // Get the project title and module version.
- $project->title = isset($projects[$name]->info['name']) ? $projects[$name]->info['name'] : '';
- $project->module_version = isset($projects[$name]->info['version']) ? $projects[$name]->info['version'] : $project->version;
-
- // Project with related language states.
- $row = theme('l10n_update_single_project_wrapper', array(
- 'project' => $project,
- 'project_status' => $project_status,
- 'languages' => $variables['languages'],
- 'available' => $variables['available'],
- 'history' => $variables['history'],
- 'updates' => $variables['updates'],
+ if (strpos($source->version, 'dev') !== FALSE) {
+ return t('No translation files are provided for development releases.');
+ }
+ if (l10n_update_use_remote_source() && $remote_path && $local_path) {
+ return t('File not found at %remote_path nor at %local_path', array(
+ '%remote_path' => $remote_path,
+ '%local_path' => $local_path,
));
-
- $rows[$project->project_type][] = array(
- 'data' => array(
- array(
- 'data' => $row,
- 'class' => 'l10n-update-wrapper collapsed',
- ),
- ),
- 'class' => array($project_class),
- );
}
-
- // Build tables of update states grouped by project type. Similar to the
- // status report by the Update module.
- $output = '';
- $project_types = array(
- 'core' => t('Drupal core'),
- 'module' => t('Modules'),
- 'theme' => t('Themes'),
- 'module-disabled' => t('Disabled modules'),
- 'theme-disabled' => t('Disabled themes'),
- );
- foreach ($project_types as $type_name => $type_label) {
- if (!empty($rows[$type_name])) {
- ksort($rows[$type_name]);
- $output .= "\n" . $type_label . "
\n";
- $output .= theme('table', array('header' => $header, 'rows' => $rows[$type_name], 'attributes' => array('class' => array('update l10n-update'))));
- }
+ elseif ($local_path) {
+ return t('File not found at %local_path', array('%local_path' => $local_path));
}
-
- // We use the core update module CSS to re-use the color definitions.
- // Plus add our own css and js.
- drupal_add_css(drupal_get_path('module', 'update') . '/update.css');
- drupal_add_css(drupal_get_path('module', 'l10n_update') . '/css/l10n_update.admin.css');
- drupal_add_js(drupal_get_path('module', 'l10n_update') . '/js/l10n_update.js');
-
- return $output;
+ return t('Translation file location could not be determined.');
}
/**
- * Format project translation state with states per language.
+ * Form element callback: After build changes to the language update table.
*
- * @param $variables
- * An associative array containing:
- * - project: Project data object
- * - project_status: Project status
- * - languages: Available languages.
- * @return string
- * HTML output.
+ * Adds labels to the languages and removes checkboxes from languages from which
+ * translation files could not be found.
*/
-function theme_l10n_update_single_project_wrapper($variables) {
- $project = $variables['project'];
- $name = $project->name;
- $project_status = $variables['project_status'];
- $languages = $variables['languages'];
- $history = $variables['history'];
- $updates = $variables['updates'];
- $availables = $variables['available'];
-
- // Output project title and project summary status.
- $output = theme('l10n_update_single_project_status', array(
- 'project' => $project,
- 'server' => l10n_update_server($project->l10n_server),
- 'status' => $project_status,
- ));
-
- // Translation status per language is displayed in a table, one language per row.
- // For each language the current translation is listed. And optionally the
- // most recent update.
- $rows = array();
- foreach ($languages as $lang => $language) {
- // Determine current translation status and update status.
- $installed = isset($history[$name][$lang]) ? $history[$name][$lang] : NULL;
- $update = isset($updates[$name][$lang]) ? $updates[$name][$lang] : NULL;
- $available = isset($availables[$name][$lang]) ? $availables[$name][$lang] : NULL;
- if ($installed) {
- if ($update) {
- $status = 'updatable';
- $class = 'messages warning';
- }
- else {
- $status = 'uptodate';
- $class = 'ok';
- }
+function l10n_update_language_table($form_element) {
+ // Remove checkboxes of languages without updates.
+ if ($form_element['#not_found']) {
+ foreach ($form_element['#not_found'] as $langcode) {
+ $form_element[$langcode] = array();
}
- elseif ($available) {
- $status = 'available';
- $class = 'warning';
+ }
+ return $form_element;
+}
+
+/**
+ * Returns HTML for translation edit form.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - form: The form that contains the language information.
+ *
+ * @see l10n_update_edit_form()
+ * @ingroup themeable
+ */
+function theme_l10n_update_edit_form_strings($variables) {
+ $output = '';
+ $form = $variables['form'];
+ $header = array(
+ t('Source string'),
+ t('Translation for @language', array('@language' => $form['#language'])),
+ );
+ $rows = array();
+ foreach (element_children($form) as $lid) {
+ $string = $form[$lid];
+ if ($string['plural']['#value']) {
+ $source = drupal_render($string['original_singular']) . '
' . drupal_render($string['original_plural']);
}
else {
- $status = 'unknown';
- $class = 'unknown';
+ $source = drupal_render($string['original']);
}
-
- // The current translation version.
- $row = theme('l10n_update_current_release', array('language' => $language, 'release' => $installed, 'status' => $status));
-
- // If an update is available, add it.
- if ($update) {
- $row .= theme('l10n_update_available_release', array('release' => $update));
- }
-
+ $source .= empty($string['context']) ? '' : '
' . t('In Context') . ': ' . $string['context']['#value'] . '';
$rows[] = array(
- 'data' => array($row),
- 'class' => array($class),
+ array('data' => $source),
+ array('data' => $string['translations']),
+ );
+ }
+ $table = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#empty' => t('No strings available.'),
+ '#attributes' => array('class' => array('locale-translate-edit-table')),
+ );
+ $output .= drupal_render($table);
+ $pager = array('#theme' => 'pager');
+ $output .= drupal_render($pager);
+ return $output;
+}
+
+/**
+ * Prepares variables for translation status information templates.
+ *
+ * Translation status information is displayed per language.
+ *
+ * Default template: l10n_update-translation-update-info.tpl.php.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - updates: The projects which have updates.
+ * - not_found: The projects which updates are not found.
+ *
+ * @see l10n_update_status_form()
+ */
+function template_preprocess_l10n_update_update_info(&$variables) {
+ $details = array();
+ $modules = array();
+
+ // Default values
+ $variables['modules'] = array();
+ $variables['module_list'] = '';
+ $details['available_updates_list'] = array();
+
+ // Build output for available updates.
+ if (isset($variables['updates'])) {
+ $releases = array();
+ if ($variables['updates']) {
+ foreach ($variables['updates'] as $update) {
+ $modules[] = $update['name'];
+ $releases[] = t('@module (@date)', array('@module' => $update['name'], '@date' => format_date($update['timestamp'], 'html_date')));
+ }
+ $variables['modules'] = $modules;
+ $variables['module_list'] = t('Updates for: @modules', array('@modules' => implode(', ', $modules)));
+ }
+ $details['available_updates_list'] = array(
+ '#theme' => 'item_list',
+ '#items' => $releases,
);
}
- // Output tables with translation status per language.
- $output .= '' . "\n";
- $output .= theme('table', array('header' => array(), 'rows' => $rows));
- $output .= "\n";
-
- return $output;
+ // Build output for updates not found.
+ if (isset($variables['not_found'])) {
+ $releases = array();
+ $variables['missing_updates_status'] = format_plural(count($variables['not_found']), 'Missing translations for one project', 'Missing translations for @count projects');
+ if ($variables['not_found']) {
+ foreach ($variables['not_found'] as $update) {
+ $version = $update['version'] ? $update['version'] : t('no version');
+ $releases[] = t('@module (@version).', array('@module' => $update['name'], '@version' => $version)) . ' ' . $update['info'];
+ }
+ }
+ $details['missing_updates_list'] = array(
+ '#theme' => 'item_list',
+ '#items' => $releases,
+ );
+ // Prefix the missing updates list if there is an available updates lists
+ // before it.
+ if (!empty($details['missing_updates_list']['#items'])) {
+ $details['missing_updates_list']['#prefix'] = t('Missing translations for:');
+ }
+ }
+ $variables['details'] = $details;
}
/**
- * Format a single project translation state.
+ * Prepares variables for most recent translation update templates.
+ *
+ * Displays the last time we checked for locale update data. In addition to
+ * properly formatting the given timestamp, this function also provides a "Check
+ * manually" link that refreshes the available update and redirects back to the
+ * same page.
+ *
+ * Default template: l10n_update-translation-last-check.tpl.php.
*
* @param $variables
* An associative array containing:
- * - project: project data object.
- * - server: (optional) remote server data object.
- * - status: project summary status.
- * @return string
- * HTML output.
- */
-function theme_l10n_update_single_project_status($variables) {
- $project = $variables['project'];
- $server = $variables['server'];
- $title = $project->title ? $project->title : $project->name;
-
- $output = '';
- $output .= '' . check_plain($title) . '' . ' ' . check_plain($project->module_version) ;
- if ($server = l10n_update_server($project->l10n_server)) {
- $output .= '' . t('(translation source: !server)', array('!server' => l($server['name'], $server['link']))) . '';
- }
- $output .= theme('l10n_update_version_status', array('status' => $variables['status']));
- $output .= "\n";
-
- return $output;
-}
-
-/**
- * Format current translation version.
+ * - last: The timestamp when the site last checked for available updates.
*
- * @param $variables
- * An associative array containing:
- * - language: Language name.
- * - release: Current file data.
- * - status: Release status.
- * @return string
- * HTML output.
+ * @see l10n_update_status_form()
*/
-function theme_l10n_update_current_release($variables) {
- if (isset($variables['release'])) {
- $date = $variables['release']->timestamp;
- $version = $variables['release']->version;
- $text = t('@language: @version (!date)', array('@language' => $variables['language'], '@version' => $version, '!date' => format_date($date, 'custom', 'Y-M-d')));
- }
- else {
- $text = t('@language: No installed translation', array('@language' => $variables['language']));
- }
-
- $output = '';
- $output .= $text;
- $output .= theme('l10n_update_version_status', $variables);
- $output .= "\n";
-
- return $output;
-}
-
-/**
- * Format current translation version.
- *
- * @param object $release
- * Update file data.
- * @return string
- * HTML output.
- */
-function theme_l10n_update_available_release($variables) {
- $date = $variables['release']->timestamp;
- $version = $variables['release']->version;
- if (!empty($variables['release']->fileurl)) {
- // Remote file, straight link
- $link = l(t('Download'), $variables['release']->fileurl);
- }
- elseif (!empty($variables['release']->uri)) {
- // Local file, try something
- $link = l(t('Download'), $variables['release']->uri, array('absolute' => TRUE));
- }
-
- $output = '';
- $output .= t('Recommended version: @version (!date)', array('@version' => $version, '!date' => format_date($date, 'custom', 'Y-M-d')));
- $output .= '' . $link . '';
- $output .= "\n";
- return $output;
-}
-
-/**
- * Format version status with icon.
- *
- * @param string $status
- * Version status: 'uptodate', 'updatable', 'available', 'unknown'.
- * @param string $type
- * Update type: 'download', 'localfile'.
- *
- * @return sting
- * HTML output.
- */
-function theme_l10n_update_version_status($variables) {
- $icon = '';
- $msg = '';
-
- switch ($variables['status']) {
- case 'uptodate':
- $icon = theme('image', array('path' => 'misc/watchdog-ok.png', 'alt' => t('ok'), 'title' => t('ok')));
- $msg = '' . t('Up to date') . '';
- break;
- case 'updatable':
- $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning')));
- $msg = '' . t('Update available') . '';
- break;
- case 'available':
- $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning')));
- $msg = '' . t('Uninstalled translation available') . '';
- break;
- case 'unknown':
- $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning')));
- $msg = '' . t('No available translations found') . '';
- break;
- }
- $output = '';
- $output .= $msg;
- $output .= '' . $icon . '';
- $output .= "\n";
- return $output;
+function template_preprocess_l10n_update_last_check(&$variables) {
+ $last = $variables['last'];
+ $variables['last_checked'] = $last ? t('Last checked: !time ago', array('!time' => format_interval(REQUEST_TIME - $last))) : t('Last checked: never');
+ $variables['link'] = l(t('Check manually'), 'admin/config/regional/translate/check');
}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.api.php b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.api.php
index 918c74fd..2d98621b 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.api.php
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.api.php
@@ -5,27 +5,6 @@
* API documentation for Localize updater module.
*/
-/**
- * Returns available translation servers and server definitions.
- *
- * @return keyed array of available servers.
- * Example: array('localize.drupal.org' => array(
- * 'name' => 'localize.drupal.org',
- * 'server_url' => 'http://ftp.drupal.org/files/translations/l10n_server.xml',
- * 'update_url' => 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po',
- * ),
- * );
- */
-function hook_l10n_servers() {
- // This hook is used to specify the default localization server(s).
- // Additionally server data can be specified on a per project basis in the
- // .info file or using the hook_l10n_update_projects_alter().
-
- module_load_include('inc', 'l10n_update');
- $server = l10n_update_default_server();
- return array($server['name'] => $server );
-}
-
/**
* Alter the list of project to be updated by l10n update.
*
@@ -46,8 +25,6 @@ function hook_l10n_update_projects_alter(&$projects) {
// the translation download path specified in the 10n_server.xml file.
$projects['existing_example_project'] = array(
'info' => array(
- 'l10n server' => 'example.com',
- 'l10n url' => 'http://example.com/files/translations/l10n_server.xml',
'l10n path' => 'http://example.com/files/translations/%core/%project/%project-%release.%language.po',
),
);
@@ -60,10 +37,9 @@ function hook_l10n_update_projects_alter(&$projects) {
'project_type' => 'module',
'name' => 'new_example_project',
'info' => array(
- 'version' => '6.x-1.5',
- 'core' => '6.x',
- 'l10n server' => 'example.com',
- 'l10n url' => 'http://example.com/files/translations/l10n_server.xml',
+ 'name' => 'New example project',
+ 'version' => '7.x-1.5',
+ 'core' => '7.x',
'l10n path' => 'http://example.com/files/translations/%core/%project/%project-%release.%language.po',
),
);
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.batch.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.batch.inc
index cc2406e6..ba53454c 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.batch.inc
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.batch.inc
@@ -2,182 +2,197 @@
/**
* @file
- * Reusable API for creating and running l10n update batches.
+ * Batch process to check the availability of remote or local po files.
*/
-// module_load_include will not work in batch.
-include_once 'l10n_update.check.inc';
-
/**
- * Create a batch to just download files.
- *
- * @param $updates
- * Translations sources to be downloaded.
- * Note: All update sources must have a 'fileurl'.
- * @return array
- * A batch definition for this download.
+ * Load the common translation API.
*/
-function l10n_update_batch_download($updates) {
- foreach ($updates as $update) {
- $id = $update->filename;
- $operations[] = array('_l10n_update_batch_download', array($id, $update));
+// @todo Combine functions differently in files to avoid unnecessary includes.
+// Follow-up issue http://drupal.org/node/1834298
+require_once __DIR__ . '/l10n_update.translation.inc';
+
+/**
+ * Batch operation callback: Check status of a remote and local po file.
+ *
+ * Checks the presence and creation time po translation files in located at
+ * remote server location and local file system.
+ *
+ * @param string $project
+ * Machine name of the project for which to check the translation status.
+ * @param string $langcode
+ * Language code of the language for which to check the translation.
+ * @param array $options
+ * Optional, an array with options that can have the following elements:
+ * - 'finish_feedback': Whether or not to give feedback to the user when the
+ * batch is finished. Optional, defaults to TRUE.
+ * - 'use_remote': Whether or not to check the remote translation file.
+ * Optional, defaults to TRUE.
+ * @param array $context
+ * The batch context.
+*/
+function l10n_update_batch_status_check($project, $langcode, $options = array(), &$context) {
+ $failure = $checked = FALSE;
+ $options += array(
+ 'finish_feedback' => TRUE,
+ 'use_remote' => TRUE,
+ );
+ $source = l10n_update_get_status(array($project), array($langcode));
+ $source = $source[$project][$langcode];
+
+ // Check the status of local translation files.
+ if (isset($source->files[L10N_UPDATE_LOCAL])) {
+ if ($file = l10n_update_source_check_file($source)) {
+ l10n_update_status_save($source->name, $source->langcode, L10N_UPDATE_LOCAL, $file);
+ }
+ $checked = TRUE;
}
- return _l10n_update_create_batch($operations);
-}
-/**
- * Create a batch to just import files.
- *
- * All update sources must have a 'uri'.
- *
- * @param $updates
- * Translations sources to be imported.
- * Note: All update sources must have a 'fileurl'.
- * @param $import_mode
- * Import mode. How to treat existing and modified translations.
- * @return array
- * A batch definition for this import.
- */
-function l10n_update_batch_import($updates, $import_mode) {
- foreach ($updates as $update) {
- $id = $update->filename;
- $operations[] = array('_l10n_update_batch_import', array($id, $update, $import_mode));
- }
- return _l10n_update_create_batch($operations);
-}
-
-/**
- * Create a big batch for multiple projects and languages.
- *
- * @param $updates
- * Array of update sources to be run.
- * @param $mode
- * Import mode. How to treat existing and modified translations.
- * @return array
- */
-function l10n_update_batch_multiple($updates, $import_mode) {
- foreach ($updates as $update) {
- $id = $update->filename;
- if ($update->type == 'download') {
- $operations[] = array('_l10n_update_batch_download', array($id, $update));
- $operations[] = array('_l10n_update_batch_import', array($id, NULL, $import_mode));
+ // Check the status of remote translation files.
+ if ($options['use_remote'] && isset($source->files[L10N_UPDATE_REMOTE])) {
+ $remote_file = $source->files[L10N_UPDATE_REMOTE];
+ module_load_include('http.inc', 'l10n_update');
+ if ($result = l10n_update_http_check($remote_file->uri)) {
+ // Update the file object with the result data. In case of a redirect we
+ // store the resulting uri.
+ if (!empty($result->updated)) {
+ $remote_file->uri = isset($result->redirect_url) ? $result->redirect_url : $remote_file->uri;
+ $remote_file->timestamp = $result->updated;
+ l10n_update_status_save($source->name, $source->langcode, L10N_UPDATE_REMOTE, $remote_file);
+ }
+ // @todo What to do with when the file is not found (404)? To prevent
+ // re-checking within the TTL (1day, 1week) we can set a last_checked
+ // timestamp or cache the result.
+ $checked = TRUE;
}
else {
- $operations[] = array('_l10n_update_batch_import', array($id, $update, $import_mode));
+ $failure = TRUE;
}
- // This one takes always parameters from results.
- $operations[] = array('_l10n_update_batch_history', array($id));
}
- if (!empty($operations)) {
- return _l10n_update_create_batch($operations);
+
+ // Provide user feedback and record success or failure for reporting at the
+ // end of the batch.
+ if ($options['finish_feedback'] && $checked) {
+ $context['results']['files'][] = $source->name;
}
+ if ($failure && !$checked) {
+ $context['results']['failed_files'][] = $source->name;
+ }
+ $context['message'] = t('Checked translation for %project.', array('%project' => $source->project));
}
/**
- * Create batch stub for this module.
+ * Batch finished callback: Set result message.
*
- * @param $operations
- * Operations to perform in this batch.
- * @return array
- * A batch definition:
- * - 'operations': Batch operations
- * - 'title': Batch title.
- * - 'init_message': Initial batch UI message.
- * - 'error_message': Batch error message.
- * - 'file': File containing callback function.
- * - 'finished': Batch completed callback function.
+ * @param boolean $success
+ * TRUE if batch successfully completed.
+ * @param array $results
+ * Batch results.
*/
-function _l10n_update_create_batch($operations = array()) {
- $t = get_t();
- $batch = array(
- 'operations' => $operations,
- 'title' => $t('Updating translation.'),
- 'init_message' => $t('Downloading and importing files.'),
- 'error_message' => $t('Error importing interface translations'),
- 'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
- 'finished' => '_l10n_update_batch_finished',
- );
- return $batch;
-}
-
-/**
- * Batch process: Download a file.
- *
- * @param $id
- * Batch id to identify batch results.
- * Result of this batch function are stored in $context['result']
- * identified by this $id.
- * @param $file
- * File to be downloaded.
- * @param $context
- * Batch context array.
- */
-function _l10n_update_batch_download($id, $file, &$context) {
- $t = get_t();
- if (l10n_update_source_download($file)) {
- $context['message'] = $t('Importing: %name.', array('%name' => $file->filename));
- $context['results'][$id] = array('file' => $file);
+function l10n_update_batch_status_finished($success, $results) {
+ if ($success) {
+ if (isset($results['failed_files'])) {
+ if (module_exists('dblog')) {
+ $message = format_plural(count($results['failed_files']), 'One translation file could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.', array('@url' => url('admin/reports/dblog')));
+ }
+ else {
+ $message = format_plural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.');
+ }
+ drupal_set_message($message, 'error');
+ }
+ if (isset($results['files'])) {
+ drupal_set_message(format_plural(
+ count($results['files']),
+ 'Checked available interface translation updates for one project.',
+ 'Checked available interface translation updates for @count projects.'
+ ));
+ }
+ if (!isset($results['failed_files']) && !isset($results['files'])) {
+ drupal_set_message(t('Nothing to check.'));
+ }
+ variable_set('l10n_update_last_check', REQUEST_TIME);
}
else {
- $context['results'][$id] = array('file' => $file, 'fail' => TRUE);
+ drupal_set_message(t('An error occurred trying to check available interface translation updates.'), 'error');
}
}
/**
- * Batch process: Update the download history table.
+ * Batch operation: Download a remote translation file.
*
- * @param $id
- * Batch id to identify batch results.
- * Result of this batch function are stored in $context['result']
- * identified by this $id.
- * @param $context
- * Batch context array.
+ * Downloads a remote gettext file into the translations directory. When
+ * successfully the translation status is updated.
+ *
+ * @param string $project
+ * Name of the translatable project.
+ * @param string $langcode
+ * Language code.
+ * @param array $context
+ * The batch context.
+ *
+ * @see l10n_update_batch_fetch_import()
*/
-function _l10n_update_batch_history($id, &$context) {
- $t = get_t();
- // The batch import is performed in a number of steps. History update is
- // always the last step. The details of the downloaded/imported file are
- // stored in $context['results'] array.
- if (isset($context['results'][$id]['file']) && !isset($context['results'][$id]['fail'])) {
- $file = $context['results'][$id]['file'];
- l10n_update_source_history($file);
- $context['message'] = $t('Imported: %name.', array('%name' => $file->filename));
+function l10n_update_batch_fetch_download($project, $langcode, &$context) {
+ $sources = l10n_update_get_status(array($project), array($langcode));
+ if (isset($sources[$project][$langcode])) {
+ $source = $sources[$project][$langcode];
+ if (isset($source->type) && $source->type == L10N_UPDATE_REMOTE) {
+ if ($file = l10n_update_download_source($source->files[L10N_UPDATE_REMOTE], 'translations://')) {
+ $context['message'] = t('Downloaded translation for %project.', array('%project' => $source->project));
+ l10n_update_status_save($source->name, $source->langcode, L10N_UPDATE_LOCAL, $file);
+ }
+ else {
+ $context['results']['failed_files'][] = $source->files[L10N_UPDATE_REMOTE];
+ }
+ }
}
}
/**
* Batch process: Import translation file.
*
- * This takes a file parameter or continues from previous batch
- * which should have downloaded a file.
+ * Imports a gettext file from the translation directory. When successfully the
+ * translation status is updated.
*
- * @param $id
- * Batch id to identify batch results.
- * Result of this batch function are stored in $context['result']
- * identified by this $id.
- * @param $file
- * File to be imported. If empty, the file will be taken from $context['results'].
- * @param $mode
- * Import mode. How to treat existing and modified translations.
- * @param $context
- * Batch context array.
+ * @param string $project
+ * Name of the translatable project.
+ * @param string $langcode
+ * Language code.
+ * @param array $options
+ * Array of import options.
+ * @param array $context
+ * The batch context.
+ *
+ * @see l10n_update_batch_import_files()
+ * @see l10n_update_batch_fetch_download()
*/
-function _l10n_update_batch_import($id, $file, $mode, &$context) {
- $t = get_t();
- // The batch import is performed in two or three steps.
- // If import is performed after file download the file details of the download
- // are used which are stored in the $context['results'] array.
- // If a locally stored file is imported, the file details are placed in $file.
- if (empty($file) && isset($context['results'][$id]['file']) && !isset($context['results'][$id]['fail'])) {
- $file = $context['results'][$id]['file'];
- }
- if ($file) {
- if ($import_result = l10n_update_source_import($file, $mode)) {
- $context['message'] = $t('Imported: %name.', array('%name' => $file->filename));
- $context['results'][$id] = array_merge((array)$context['results'][$id], $import_result, array('file' => $file));
- }
- else {
- $context['results'][$id] = array_merge((array)$context['results'][$id], array('fail' => TRUE), array('file' => $file));
+function l10n_update_batch_fetch_import($project, $langcode, $options, &$context) {
+ $sources = l10n_update_get_status(array($project), array($langcode));
+ if (isset($sources[$project][$langcode])) {
+ $source = $sources[$project][$langcode];
+ if (isset($source->type)) {
+ if ($source->type == L10N_UPDATE_REMOTE || $source->type == L10N_UPDATE_LOCAL) {
+ $file = $source->files[L10N_UPDATE_LOCAL];
+ module_load_include('bulk.inc', 'l10n_update');
+ $options += array(
+ 'message' => t('Importing translation for %project.', array('%project' => $source->project)),
+ );
+ // Import the translation file. For large files the batch operations is
+ // progressive and will be called repeatedly until finished.
+ l10n_update_batch_import($file, $options, $context);
+
+ // The import is finished.
+ if (isset($context['finished']) && $context['finished'] == 1) {
+ // The import is successful.
+ if (isset($context['results']['files'][$file->uri])) {
+ $context['message'] = t('Imported translation for %project.', array('%project' => $source->project));
+
+ // Save the data of imported source into the {l10n_update_file} table and
+ // update the current translation status.
+ l10n_update_status_save($project, $langcode, L10N_UPDATE_CURRENT, $source->files[L10N_UPDATE_LOCAL]);
+ }
+ }
+ }
}
}
}
@@ -185,84 +200,47 @@ function _l10n_update_batch_import($id, $file, $mode, &$context) {
/**
* Batch finished callback: Set result message.
*
- * @param $success
- * TRUE if batch succesfully completed.
- * @param $results
+ * @param boolean $success
+ * TRUE if batch successfully completed.
+ * @param array
* Batch results.
*/
-function _l10n_update_batch_finished($success, $results) {
- $totals = array(); // Sum of added, updated and deleted translations.
- $total_skip = 0; // Sum of skipped translations
- $messages = array(); // User feedback messages.
- $project_fail = $project_success = array(); // Project names of succesfull and failed imports.
- $t = get_t();
-
+function l10n_update_batch_fetch_finished($success, $results) {
+ module_load_include('bulk.inc', 'l10n_update');
if ($success) {
- // Summarize results of added, updated, deleted and skiped translations.
- // Added, updated and deleted are summarized per language to be displayed accordingly.
- foreach ($results as $result) {
- if (isset($result['fail'])) {
- // Collect project names of the failed imports.
- $project_fail[$result['file']->name] = $result['file']->name;
- }
- else {
- $language = $result['language'];
-
- // Initialize variables to prevent PHP Notices.
- if (!isset($totals[$language])) {
- $totals[$language] = array();
- $totals[$language]['add'] = $totals[$language]['update'] = $totals[$language]['delete'] = 0;
- }
-
- // Summarize added, updated, deleted and skiped translations.
- $totals[$language]['add'] += $result['add'];
- $totals[$language]['update'] += $result['update'];
- $totals[$language]['delete'] += $result['delete'];
- $total_skip += $result['skip'];
-
- // Collect project names of the succesfull imports.
- $project_success[$result['file']->name] = $result['file']->name;
- }
- }
-
- // Messages of succesfull translation update results.
- if ($project_success) {
- $messages[] = format_plural(count($project_success), 'One project updated: @projects.', '@count projects updated: @projects.', array('@projects' => implode(', ', $project_success)));
- $languages = language_list();
- foreach ($totals as $language => $total) {
- $messages[] = $t('%language translation strings added: !add, updated: !update, deleted: !delete.', array(
- '%language' => $languages[$language]->name,
- '!add' => $total['add'],
- '!update' => $total['update'],
- '!delete' => $total['delete'],
- ));
- }
- drupal_set_message(implode("
\n", $messages));
-
- // Warning for disallowed HTML.
- if ($total_skip) {
- drupal_set_message(
- format_plural(
- $total_skip,
- 'One translation string was skipped because it contains disallowed HTML. See !log_messages for details.',
- '@count translation strings were skipped because they contain disallowed HTML. See !log_messages for details.',
- array('!log_messages' => l(t('Recent log messages'), 'admin/reports/dblog'))),
- 'warning');
- }
- }
-
- // Error for failed imports.
- if ($project_fail) {
- drupal_set_message(
- format_plural(
- count($project_fail),
- 'Translations of one project were not imported: @projects.',
- 'Translations of @count projects were not imported: @projects',
- array('@projects' => implode(', ', $project_fail))),
- 'error');
- }
- }
- else {
- drupal_set_message($t('Error importing translations.'), 'error');
+ variable_set('l10n_update_last_check', REQUEST_TIME);
}
+ l10n_update_batch_finished($success, $results);
+}
+
+
+
+/**
+ * Downloads a translation file from a remote server.
+ *
+ * @param object $source_file
+ * Source file object with at least:
+ * - "uri": uri to download the file from.
+ * - "project": Project name.
+ * - "langcode": Translation language.
+ * - "version": Project version.
+ * - "filename": File name.
+ * @param string $directory
+ * Directory where the downloaded file will be saved. Defaults to the
+ * temporary file path.
+ *
+ * @return object
+ * File object if download was successful. FALSE on failure.
+ */
+function l10n_update_download_source($source_file, $directory = 'temporary://') {
+ if ($uri = system_retrieve_file($source_file->uri, $directory)) {
+ $file = clone($source_file);
+ $file->type = L10N_UPDATE_LOCAL;
+ $file->uri = $uri;
+ $file->directory = $directory;
+ $file->timestamp = filemtime($uri);
+ return $file;
+ }
+ watchdog('l10n_update', 'Unable to download translation file @uri.', array('@uri' => $source_file->uri), WATCHDOG_ERROR);
+ return FALSE;
}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.bulk.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.bulk.inc
new file mode 100644
index 00000000..eee6f19a
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.bulk.inc
@@ -0,0 +1,726 @@
+ $language) {
+ if ($langcode != 'en' || l10n_update_english()) {
+ $existing_languages[$langcode] = $language->name;
+ }
+ }
+
+ // If we have no languages available, present the list of predefined languages
+ // only. If we do have already added languages, set up two option groups with
+ // the list of existing and then predefined languages.
+ form_load_include($form_state, 'inc', 'language', 'language.admin');
+ if (empty($existing_languages)) {
+ $language_options = language_admin_predefined_list();
+ $default = key($language_options);
+ }
+ else {
+ $default = key($existing_languages);
+ $language_options = array(
+ t('Existing languages') => $existing_languages,
+ t('Languages not yet added') => language_admin_predefined_list()
+ );
+ }
+
+ $validators = array(
+ 'file_validate_extensions' => array('po'),
+ 'file_validate_size' => array(file_upload_max_size()),
+ );
+ $form['file'] = array(
+ '#type' => 'file',
+ '#title' => t('Translation file'),
+ '#description' => theme('file_upload_help', array('description' => t('A Gettext Portable Object file.'), 'upload_validators' => $validators)),
+ '#size' => 50,
+ '#upload_validators' => $validators,
+ '#attributes' => array('class' => array('file-import-input')),
+ '#attached' => array(
+ 'js' => array(
+ drupal_get_path('module', 'locale') . '/locale.bulk.js' => array(),
+ ),
+ ),
+ );
+ $form['langcode'] = array(
+ '#type' => 'select',
+ '#title' => t('Language'),
+ '#options' => $language_options,
+ '#default_value' => $default,
+ '#attributes' => array('class' => array('langcode-input')),
+ );
+
+ $form['customized'] = array(
+ '#title' => t('Treat imported strings as custom translations'),
+ '#type' => 'checkbox',
+ );
+ $form['overwrite_options'] = array(
+ '#type' => 'container',
+ '#tree' => TRUE,
+ );
+ $form['overwrite_options']['not_customized'] = array(
+ '#title' => t('Overwrite non-customized translations'),
+ '#type' => 'checkbox',
+ '#states' => array(
+ 'checked' => array(
+ ':input[name="customized"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+ $form['overwrite_options']['customized'] = array(
+ '#title' => t('Overwrite existing customized translations'),
+ '#type' => 'checkbox',
+ );
+
+ $form['actions'] = array(
+ '#type' => 'actions'
+ );
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Import')
+ );
+ return $form;
+}
+
+/**
+ * Form submission handler for l10n_update_import_form().
+ */
+function l10n_update_import_form_submit($form, &$form_state) {
+ // Ensure we have the file uploaded.
+ if ($file = file_save_upload('file', $form_state, $form['file']['#upload_validators'], 'translations://', 0)) {
+
+ // Add language, if not yet supported.
+ $language = language_load($form_state['values']['langcode']);
+ if (empty($language)) {
+ $language = new Language(array(
+ 'id' => $form_state['values']['langcode']
+ ));
+ $language = language_save($language);
+ drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name))));
+ }
+ $options = array(
+ 'langcode' => $form_state['values']['langcode'],
+ 'overwrite_options' => $form_state['values']['overwrite_options'],
+ 'customized' => $form_state['values']['customized'] ? L10N_UPDATE_CUSTOMIZED : L10N_UPDATE_NOT_CUSTOMIZED,
+ );
+ $file = l10n_update_file_attach_properties($file, $options);
+ $batch = l10n_update_batch_build(array($file->uri => $file), $options);
+ batch_set($batch);
+ }
+ else {
+ form_set_error('file', $form_state, t('File to import not found.'));
+ $form_state['rebuild'] = TRUE;
+ return;
+ }
+
+ $form_state['redirect_route']['route_name'] = 'locale.translate_page';
+ return;
+}
+
+/**
+ * Form constructor for the Gettext translation files export form.
+ *
+ * @see l10n_update_export_form_submit()
+ * @ingroup forms
+ */
+function l10n_update_export_form($form, &$form_state) {
+ global $language;
+ $languages = language_list();
+ $language_options = array();
+ foreach ($languages as $langcode => $language) {
+ if ($langcode != 'en' || l10n_update_english()) {
+ $language_options[$langcode] = $language->name;
+ }
+ }
+ $language_default = language_default();
+
+ if (empty($language_options)) {
+ $form['langcode'] = array(
+ '#type' => 'value',
+ '#value' => $language->language,
+ );
+ $form['langcode_text'] = array(
+ '#type' => 'item',
+ '#title' => t('Language'),
+ '#markup' => t('No language available. The export will only contain source strings.'),
+ );
+ }
+ else {
+ $form['langcode'] = array(
+ '#type' => 'select',
+ '#title' => t('Language'),
+ '#options' => $language_options,
+ '#default_value' => $language_default->id,
+ '#empty_option' => t('Source text only, no translations'),
+ '#empty_value' => $language->language,
+ );
+ $form['content_options'] = array(
+ '#type' => 'details',
+ '#title' => t('Export options'),
+ '#collapsed' => TRUE,
+ '#tree' => TRUE,
+ '#states' => array(
+ 'invisible' => array(
+ ':input[name="langcode"]' => array('value' => $language->language),
+ ),
+ ),
+ );
+ $form['content_options']['not_customized'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include non-customized translations'),
+ '#default_value' => TRUE,
+ );
+ $form['content_options']['customized'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include customized translations'),
+ '#default_value' => TRUE,
+ );
+ $form['content_options']['not_translated'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include untranslated text'),
+ '#default_value' => TRUE,
+ );
+ }
+
+ $form['actions'] = array(
+ '#type' => 'actions'
+ );
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Export')
+ );
+ return $form;
+}
+
+/**
+ * Form submission handler for l10n_update_export_form().
+ */
+function l10n_update_export_form_submit($form, &$form_state) {
+ global $language;
+
+ // If template is required, language code is not given.
+ if ($form_state['values']['langcode'] != $language->language) {
+ $languages = language_list();
+ $language = isset($languages[$form_state['values']['langcode']]) ? $languages[$form_state['values']['langcode']] : NULL;
+ }
+ else {
+ $language = NULL;
+ }
+ $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
+ $reader = new PoDatabaseReader();
+ $languageName = '';
+ if ($language != NULL) {
+ $reader->setLangcode($language->id);
+ $reader->setOptions($content_options);
+ $languages = language_list();
+ $languageName = isset($languages[$language->id]) ? $languages[$language->id]->name : '';
+ $filename = $language->id .'.po';
+ }
+ else {
+ // Template required.
+ $filename = 'drupal.pot';
+ }
+
+ $item = $reader->readItem();
+ if (!empty($item)) {
+ $uri = tempnam('temporary://', 'po_');
+ $header = $reader->getHeader();
+ $header->setProjectName(variable_get('site_name'));
+ $header->setLanguageName($languageName);
+
+ $writer = new PoStreamWriter;
+ $writer->setUri($uri);
+ $writer->setHeader($header);
+
+ $writer->open();
+ $writer->writeItem($item);
+ $writer->writeItems($reader);
+ $writer->close();
+ }
+ else {
+ drupal_set_message('Nothing to export.');
+ }
+}
+
+/**
+ * Prepare a batch to import all translations.
+ *
+ * @param array $options
+ * An array with options that can have the following elements:
+ * - 'langcode': The language code. Optional, defaults to NULL, which means
+ * that the language will be detected from the name of the files.
+ * - 'overwrite_options': Overwrite options array as defined in
+ * PoDatabaseWriter. Optional, defaults to an empty array.
+ * - 'customized': Flag indicating whether the strings imported from $file
+ * are customized translations or come from a community source. Use
+ * L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
+ * L10N_UPDATE_NOT_CUSTOMIZED.
+ * - 'finish_feedback': Whether or not to give feedback to the user when the
+ * batch is finished. Optional, defaults to TRUE.
+ *
+ * @param $force
+ * (optional) Import all available files, even if they were imported before.
+ *
+ * @todo
+ * Integrate with update status to identify projects needed and integrate
+ * l10n_update functionality to feed in translation files alike.
+ * See http://drupal.org/node/1191488.
+ */
+function l10n_update_batch_import_files($options, $force = FALSE) {
+ $options += array(
+ 'overwrite_options' => array(),
+ 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
+ 'finish_feedback' => TRUE,
+ );
+
+ if (!empty($options['langcode'])) {
+ $langcodes = array($options['langcode']);
+ }
+ else {
+ // If langcode was not provided, make sure to only import files for the
+ // languages we have enabled.
+ $langcodes = array_keys(language_list());
+ }
+
+ $files = l10n_update_get_interface_translation_files(array(), $langcodes);
+
+ if (!$force) {
+ $result = db_select('l10n_update_file', 'lf')
+ ->fields('lf', array('langcode', 'uri', 'timestamp'))
+ ->condition('language', $langcodes)
+ ->execute()
+ ->fetchAllAssoc('uri');
+ foreach ($result as $uri => $info) {
+ if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
+ // The file is already imported and not changed since the last import.
+ // Remove it from file list and don't import it again.
+ unset($files[$uri]);
+ }
+ }
+ }
+ return l10n_update_batch_build($files, $options);
+}
+
+/**
+ * Get interface translation files present in the translations directory.
+ *
+ * @param array $projects
+ * Project names from which to get the translation files and history.
+ * Defaults to all projects.
+ * @param array $langcodes
+ * Language codes from which to get the translation files and history.
+ * Defaults to all languagues
+ *
+ * @return array
+ * An array of interface translation files keyed by their URI.
+ */
+function l10n_update_get_interface_translation_files($projects = array(), $langcodes = array()) {
+ module_load_include('compare.inc', 'l10n_update');
+ $files = array();
+ $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+
+ // Scan the translations directory for files matching a name pattern
+ // containing a project name and language code: {project}.{langcode}.po or
+ // {project}-{version}.{langcode}.po.
+ // Only files of known projects and languages will be returned.
+ $directory = variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH);
+ $result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE));
+
+ foreach ($result as $file) {
+ // Update the file object with project name and version from the file name.
+ $file = l10n_update_file_attach_properties($file);
+ if (in_array($file->project, $projects)) {
+ if (in_array($file->langcode, $langcodes)) {
+ $files[$file->uri] = $file;
+ }
+ }
+ }
+
+ return $files;
+}
+
+/**
+ * Build a locale batch from an array of files.
+ *
+ * @param $files
+ * Array of file objects to import.
+ *
+ * @param array $options
+ * An array with options that can have the following elements:
+ * - 'langcode': The language code. Optional, defaults to NULL, which means
+ * that the language will be detected from the name of the files.
+ * - 'overwrite_options': Overwrite options array as defined in
+ * PoDatabaseWriter. Optional, defaults to an empty array.
+ * - 'customized': Flag indicating whether the strings imported from $file
+ * are customized translations or come from a community source. Use
+ * L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
+ * L10N_UPDATE_NOT_CUSTOMIZED.
+ * - 'finish_feedback': Whether or not to give feedback to the user when the
+ * batch is finished. Optional, defaults to TRUE.
+ *
+ * @return
+ * A batch structure or FALSE if $files was empty.
+ */
+function l10n_update_batch_build($files, $options) {
+ $options += array(
+ 'overwrite_options' => array(),
+ 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
+ 'finish_feedback' => TRUE,
+ );
+ if (count($files)) {
+ $operations = array();
+ foreach ($files as $file) {
+ // We call l10n_update_batch_import for every batch operation.
+ $operations[] = array('l10n_update_batch_import', array($file, $options));
+ }
+ // Save the translation status of all files.
+ $operations[] = array('l10n_update_batch_import_save', array());
+
+ // Add a final step to refresh JavaScript and configuration strings.
+ $operations[] = array('l10n_update_batch_refresh', array());
+
+ $batch = array(
+ 'operations' => $operations,
+ 'title' => t('Importing interface translations'),
+ 'progress_message' => '',
+ 'error_message' => t('Error importing interface translations'),
+ 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
+ );
+ if ($options['finish_feedback']) {
+ $batch['finished'] = 'l10n_update_batch_finished';
+ }
+ return $batch;
+ }
+ return FALSE;
+}
+
+/**
+ * Perform interface translation import as a batch step.
+ *
+ * @param object $file
+ * A file object of the gettext file to be imported. The file object must
+ * contain a language parameter. This is used as the language of the import.
+ *
+ * @param array $options
+ * An array with options that can have the following elements:
+ * - 'langcode': The language code.
+ * - 'overwrite_options': Overwrite options array as defined in
+ * PoDatabaseWriter. Optional, defaults to an empty array.
+ * - 'customized': Flag indicating whether the strings imported from $file
+ * are customized translations or come from a community source. Use
+ * L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
+ * L10N_UPDATE_NOT_CUSTOMIZED.
+ * - 'message': Alternative message to display during import. Note, this must
+ * be sanitized text.
+ *
+ * @param $context
+ * Contains a list of files imported.
+ */
+function l10n_update_batch_import($file, $options, &$context) {
+ // Merge the default values in the $options array.
+ $options += array(
+ 'overwrite_options' => array(),
+ 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
+ );
+
+ if (isset($file->langcode)) {
+
+ try {
+ if (empty($context['sandbox'])) {
+ $context['sandbox']['parse_state'] = array(
+ 'filesize' => filesize(drupal_realpath($file->uri)),
+ 'chunk_size' => 200,
+ 'seek' => 0,
+ );
+ }
+ // Update the seek and the number of items in the $options array().
+ $options['seek'] = $context['sandbox']['parse_state']['seek'];
+ $options['items'] = $context['sandbox']['parse_state']['chunk_size'];
+ $report = Gettext::fileToDatabase($file, $options);
+ // If not yet finished with reading, mark progress based on size and
+ // position.
+ if ($report['seek'] < filesize($file->uri)) {
+
+ $context['sandbox']['parse_state']['seek'] = $report['seek'];
+ // Maximize the progress bar at 95% before completion, the batch API
+ // could trigger the end of the operation before file reading is done,
+ // because of floating point inaccuracies. See
+ // http://drupal.org/node/1089472
+ $context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
+ if (isset($options['message'])) {
+ $context['message'] = t('!message (@percent%).', array('!message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)));
+ }
+ else {
+ $context['message'] = t('Importing translation file: %filename (@percent%).', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)));
+ }
+ }
+ else {
+ // We are finished here.
+ $context['finished'] = 1;
+
+ // Store the file data for processing by the next batch operation.
+ $file->timestamp = filemtime($file->uri);
+ $context['results']['files'][$file->uri] = $file;
+ $context['results']['languages'][$file->uri] = $file->langcode;
+ }
+
+ // Add the reported values to the statistics for this file.
+ // Each import iteration reports statistics in an array. The results of
+ // each iteration are added and merged here and stored per file.
+ if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
+ $context['results']['stats'][$file->uri] = array();
+ }
+ foreach ($report as $key => $value) {
+ if (is_numeric($report[$key])) {
+ if (!isset($context['results']['stats'][$file->uri][$key])) {
+ $context['results']['stats'][$file->uri][$key] = 0;
+ }
+ $context['results']['stats'][$file->uri][$key] += $report[$key];
+ }
+ elseif (is_array($value)) {
+ $context['results']['stats'][$file->uri] += array($key => array());
+ $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
+ }
+ }
+ }
+ catch (Exception $exception) {
+ // Import failed. Store the data of the failing file.
+ $context['results']['failed_files'][] = $file;
+ watchdog('l10n_update', 'Unable to import translations file: @file (@message)', array('@file' => $file->uri, '@message' => $exception->getMessage()));
+ }
+ }
+}
+
+/**
+ * Batch callback: Save data of imported files.
+ *
+ * @param $context
+ * Contains a list of imported files.
+ */
+function l10n_update_batch_import_save($context) {
+ if (isset($context['results']['files'])) {
+ foreach ($context['results']['files'] as $file) {
+ // Update the file history if both project and version are known. This
+ // table is used by the automated translation update function which tracks
+ // translation status of module and themes in the system. Other
+ // translation files are not tracked and are therefore not stored in this
+ // table.
+ if ($file->project && $file->version) {
+ $file->last_checked = REQUEST_TIME;
+ l10n_update_update_file_history($file);
+ }
+ }
+ $context['message'] = t('Translations imported.');
+ }
+}
+
+/**
+ * Refreshs translations after importing strings.
+ *
+ * @param array $context
+ * Contains a list of strings updated and information about the progress.
+ */
+function l10n_update_batch_refresh(array &$context) {
+ if (!isset($context['sandbox']['refresh'])) {
+ $strings = $langcodes = array();
+ if (isset($context['results']['stats'])) {
+ // Get list of unique string identifiers and language codes updated.
+ $langcodes = array_unique(array_values($context['results']['languages']));
+ foreach ($context['results']['stats'] as $report) {
+ $strings = array_merge($strings, $report['strings']);
+ }
+ }
+ if ($strings) {
+ // Initialize multi-step string refresh.
+ $context['message'] = t('Updating translations for JavaScript and configuration strings.');
+ $context['sandbox']['refresh']['strings'] = array_unique($strings);
+ $context['sandbox']['refresh']['languages'] = $langcodes;
+ $context['sandbox']['refresh']['names'] = array();
+ $context['results']['stats']['config'] = 0;
+ $context['sandbox']['refresh']['count'] = count($strings);
+
+ // We will update strings on later steps.
+ $context['finished'] = 1 - 1 / $context['sandbox']['refresh']['count'];
+ }
+ else {
+ $context['finished'] = 1;
+ }
+ }
+ elseif (!empty($context['sandbox']['refresh']['strings'])) {
+ // Not perfect but will give some indication of progress.
+ $context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count'];
+ // Pending strings, refresh 100 at a time, get next pack.
+ $next = array_slice($context['sandbox']['refresh']['strings'], 0, 100);
+ array_splice($context['sandbox']['refresh']['strings'], 0, count($next));
+ // Clear cache and force refresh of JavaScript translations.
+ _l10n_update_refresh_translations($context['sandbox']['refresh']['languages'], $next);
+ }
+ else {
+ $context['finished'] = 1;
+ }
+}
+
+/**
+ * Finished callback of system page locale import batch.
+ */
+function l10n_update_batch_finished($success, $results) {
+ if ($success) {
+ $additions = $updates = $deletes = $skips = $config = 0;
+ if (isset($results['failed_files'])) {
+ if (module_exists('dblog')) {
+ $message = format_plural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.', array('@url' => url('admin/reports/dblog')));
+ }
+ else {
+ $message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.');
+ }
+ drupal_set_message($message, 'error');
+ }
+ if (isset($results['files'])) {
+ $skipped_files = array();
+ // If there are no results and/or no stats (eg. coping with an empty .po
+ // file), simply do nothing.
+ if ($results && isset($results['stats'])) {
+ foreach ($results['stats'] as $filepath => $report) {
+ $additions += $report['additions'];
+ $updates += $report['updates'];
+ $deletes += $report['deletes'];
+ $skips += $report['skips'];
+ if ($report['skips'] > 0) {
+ $skipped_files[] = $filepath;
+ }
+ }
+ }
+ drupal_set_message(format_plural(count($results['files']),
+ 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+ '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+ array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)
+ ));
+ watchdog('l10n_update', 'Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
+
+ if ($skips) {
+ if (module_exists('dblog')) {
+ $message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog')));
+ }
+ else {
+ $message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
+ }
+ drupal_set_message($message, 'warning');
+ watchdog('l10n_update', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING);
+ }
+ }
+ }
+}
+
+/**
+ * Creates a file object and populates the timestamp property.
+ *
+ * @param $filepath
+ * The filepath of a file to import.
+ *
+ * @return
+ * An object representing the file.
+ */
+function l10n_update_file_create($filepath) {
+ $file = new stdClass();
+ $file->filename = drupal_basename($filepath);
+ $file->uri = $filepath;
+ $file->timestamp = filemtime($file->uri);
+ return $file;
+}
+
+/**
+ * Generates file properties from filename and options.
+ *
+ * An attempt is made to determine the translation language, project name and
+ * project version from the file name. Supported file name patterns are:
+ * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
+ * Alternatively the translation language can be set using the $options.
+ *
+ * @param object $file
+ * A file object of the gettext file to be imported.
+ * @param array $options
+ * An array with options:
+ * - 'langcode': The language code. Overrides the file language.
+ *
+ * @return object
+ * Modified file object.
+ */
+function l10n_update_file_attach_properties($file, $options = array()) {
+ // If $file is a file entity, convert it to a stdClass.
+ if ($file instanceof FileInterface) {
+ $file = (object) array(
+ 'filename' => $file->getFilename(),
+ 'uri' => $file->getFileUri(),
+ );
+ }
+
+ // Extract project, version and language code from the file name. Supported:
+ // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
+ preg_match('!
+ ( # project OR project and version OR emtpy (group 1)
+ ([a-z_]+) # project name (group 2)
+ \. # .
+ | # OR
+ ([a-z_]+) # project name (group 3)
+ \- # -
+ ([0-9a-z\.\-\+]+) # version (group 4)
+ \. # .
+ | # OR
+ ) # (empty)
+ ([^\./]+) # language code (group 5)
+ \. # .
+ po # po extension
+ $!x', $file->filename, $matches);
+ if (isset($matches[5])) {
+ $file->project = $matches[2] . $matches[3];
+ $file->version = $matches[4];
+ $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
+ }
+ return $file;
+}
+
+/**
+ * Deletes interface translation files and translation history records.
+ *
+ * @param array $projects
+ * Project names from which to delete the translation files and history.
+ * Defaults to all projects.
+ * @param array $langcodes
+ * Language codes from which to delete the translation files and history.
+ * Defaults to all languagues
+ *
+ * @return boolean
+ * TRUE if files are removed successfully. FALSE if one or more files could
+ * not be deleted.
+ */
+function l10n_update_delete_translation_files($projects = array(), $langcodes = array()) {
+ $fail = FALSE;
+ l10n_update_file_history_delete($projects, $langcodes);
+
+ // Delete all translation files from the translations directory.
+ if ($files = l10n_update_get_interface_translation_files($projects, $langcodes)) {
+ foreach ($files as $file) {
+ $success = file_unmanaged_delete($file->uri);
+ if (!$success) {
+ $fail = TRUE;
+ }
+ }
+ }
+ return !$fail;
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.check.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.check.inc
deleted file mode 100644
index 066ae1f3..00000000
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.check.inc
+++ /dev/null
@@ -1,488 +0,0 @@
-created > REQUEST_TIME - $frequency)) {
- return $cache->data;
- }
- else {
- $projects = l10n_update_get_projects(TRUE);
- $languages = l10n_update_language_list();
- $local = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL;
- $remote = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE;
- $available = l10n_update_check_projects($projects, array_keys($languages), $local, $remote);
- cache_set('l10n_update_available_releases', $available, 'cache_l10n_update', $frequency ? REQUEST_TIME + $frequency : CACHE_PERMANENT);
- return $available;
- }
-}
-
-/**
- * Check latest release for project, language.
- *
- * @param $projects
- * Projects to check (objects).
- * @param $languages
- * Array of language codes to check, none to check all.
- * @param $check_local
- * Check local translation file.
- * @param $check_remote
- * Check remote translation file.
- *
- * @return array
- * Available sources indexed by project, language.
- */
-function l10n_update_check_projects($projects, $languages = NULL, $check_local = TRUE, $check_remote = TRUE) {
- $languages = $languages ? $languages : array_keys(l10n_update_language_list());
- $result = array();
- foreach ($projects as $name => $project) {
- foreach ($languages as $lang) {
- $source = l10n_update_source_build($project, $lang);
- if ($update = l10n_update_source_check($source, $check_local, $check_remote)) {
- $result[$name][$lang] = $update;
- }
- }
- }
- return $result;
-}
-
-/**
- * Compare available releases with history and get list of downloadable updates.
- *
- * @param $history
- * Update history of projects.
- * @param $available
- * Available project releases.
- * @return array
- * Projects to be updated: 'not yet downloaded', 'newer timestamp available',
- * 'new version available'.
- * Up to date projects are not included in the array.
- */
-function l10n_update_build_updates($history, $available) {
- $updates = array();
- foreach ($available as $name => $project_updates) {
- foreach ($project_updates as $lang => $update) {
- if (!empty($update->timestamp)) {
- $current = !empty($history[$name][$lang]) ? $history[$name][$lang] : NULL;
- // Add when not current, timestamp newer or version difers (newer version)
- if (_l10n_update_source_compare($current, $update) == -1 || $current->version != $update->version) {
- $updates[$name][$lang] = $update;
- }
- }
- }
- }
- return $updates;
-}
-
-/**
- * Check updates for active projects and languages.
- *
- * @param $count
- * Number of package translations to check.
- * @param $before
- * Unix timestamp, check only updates that haven't been checked for this time.
- * @param $limit
- * Maximum number of updates to do. We check $count translations
- * but we stop after we do $limit updates.
- * @return array
- */
-function l10n_update_check_translations($count, $before, $limit = 1) {
- $projects = l10n_update_get_projects();
- $updated = $checked = array();
-
- // Select active projects x languages ordered by last checked time
- $q = db_select('l10n_update_project', 'p');
- $q->leftJoin('l10n_update_file', 'f', 'p.name = f.project');
- $q->innerJoin('languages', 'l', 'l.language = f.language');
- $q->condition('p.status', 1);
- $q->condition('l.enabled', 1);
- // If the file is not there, or it is there, but we did not check since $before.
- $q->condition(db_or()->isNull('f.status')->condition(db_and()->condition('f.status', 1)->condition('f.last_checked', $before, '<')));
- $q->range(0, $count);
- $q->fields('p', array('name'));
- $q->fields('f');
- $q->addField('l', 'language', 'lang');
- $q->orderBy('last_checked');
- $result = $q->execute();
-
- if ($result) {
- $local = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL;
- $remote = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE;
- foreach ($result as $check) {
- if (count($updated) >= $limit) {
- break;
- }
- $checked[] = $check;
- if (!empty($projects[$check->name])) {
- $project = $projects[$check->name];
- $update = NULL;
- $source = l10n_update_source_build($project, $check->lang);
- $current = $check->filename ? $check : NULL;
- if ($available = l10n_update_source_check($source, $local, $remote)) {
- if (!$current || _l10n_update_source_compare($current, $available) == -1 || $current->version != $available->version) {
- $update = $available;
- }
- }
- if ($update) {
- // The update functions will update data and timestamps too
- l10n_update_source_update($update, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
- $updated[] = $update;
- }
- elseif ($current) {
- // No update available, just update timestamp for this row
- db_update('l10n_update_file')
- ->fields(array(
- 'last_checked' => REQUEST_TIME,
- ))
- ->condition('project', $current->project)
- ->condition('language', $current->language)
- ->execute();
- }
- elseif ($source) {
- // Create a new record just for keeping last checked time
- $source->last_checked = REQUEST_TIME;
- drupal_write_record('l10n_update_file', $source);
- }
- }
- }
- }
- return array($checked, $updated);
-}
-
-/**
- * Build abstract translation source, to be mapped to a file or a download.
- *
- * @param $project
- * Project object.
- * @param $langcode
- * Language code.
- * @param $filename
- * File name of translation file. May contains placeholders.
- * @return object
- * Source object, which may have these properties:
- * - 'project': Project name.
- * - 'language': Language code.
- * - 'type': Source type 'download' or 'localfile'.
- * - 'uri': Local file path.
- * - 'fileurl': Remote file URL for downloads.
- * - 'filename': File name.
- * - 'keep': TRUE to keep the downloaded file.
- * - 'timestamp': Last update time of the file.
- */
-function l10n_update_source_build($project, $langcode, $filename = L10N_UPDATE_DEFAULT_FILENAME) {
- $source = clone $project;
- $source->project = $project->name;
- $source->language = $langcode;
- $source->filename = l10n_update_build_string($source, $filename);
- return $source;
-}
-
-/**
- * Check local and remote sources for the file.
- *
- * @param $source
- * Translation source object.
- * @see l10n_update_source_build()
- * @param $check_local
- * File object of local translation file.
- * @param $check_remote
- * File object of remote translation file.
- * @return object
- * File object of most recent translation; local or remote.
- */
-function l10n_update_source_check($source, $check_local = TRUE, $check_remote = TRUE) {
- $local = $remote = NULL;
- if ($check_local) {
- $check = clone $source;
- if (l10n_update_source_check_file($check)) {
- $local = $check;
- }
- }
- if ($check_remote) {
- $check = clone $source;
- if (l10n_update_source_check_download($check)) {
- $remote = $check;
- }
- }
-
- // Get remote if newer than local only, they both can be empty
- return _l10n_update_source_compare($local, $remote) < 0 ? $remote : $local;
-}
-
-/**
- * Check remote file object.
- *
- * @param $source
- * Remote translation file object. The object will be update
- * with data of the remote file:
- * - 'type': Fixed value 'download'.
- * - 'fileurl': File name and path.
- * - 'timestamp': Last updated time.
- * @see l10n_update_source_build()
- * @return object
- * An object containing the HTTP request headers, response code, headers,
- * data, redirect status and updated timestamp.
- * NULL if failure.
- */
-function l10n_update_source_check_download($source) {
- $url = l10n_update_build_string($source, $source->l10n_path);
- $result = l10n_update_http_check($url);
- if ($result && !empty($result->updated)) {
- $source->type = 'download';
- // There may have been redirects so we store the resulting url
- $source->fileurl = isset($result->redirect_url) ? $result->redirect_url : $url;
- $source->timestamp = $result->updated;
- return $result;
- }
-}
-
-/**
- * Check whether we've got the file in the filesystem under 'translations'.
- *
- * It will search, similar to modules and themes:
- * - translations
- * - sites/all/translations
- * - sites/mysite/translations
- *
- * Using name as the key will return just the last one found.
- *
- * @param $source
- * Translation file object. The object will be updated with data of local file.
- * - 'type': Fixed value 'localfile'.
- * - 'uri': File name and path.
- * - 'timestamp': Last updated time.
- * @see l10n_update_source_build()
- * @param $directory
- * Files directory.
- * @return Object
- * File object (filename, basename, name)
- * NULL if failure.
- */
-function l10n_update_source_check_file($source, $directory = 'translations') {
- $filename = '/' . preg_quote($source->filename) . '$/';
-
- // Using the 'name' key will return
- if ($files = drupal_system_listing($filename, $directory, 'name', 0)) {
- $file = current($files);
- $source->type = 'localfile';
- $source->uri = $file->uri;
- $source->timestamp = filemtime($file->uri);
- return $file;
- }
-}
-
-/**
- * Download and import or just import source, depending on type.
- *
- * @param $source
- * Translation source object with information about the file location.
- * Object will be updated with :
- * - 'last_checked': Timestamp of current time;
- * - 'import_date': Timestamp of current time;
- * @param $mode
- * Download mode. How to treat exising and modified translations.
- * @return boolean
- * TRUE on success, NULL on failure.
- */
-function l10n_update_source_update($source, $mode) {
- if ($source->type == 'localfile' || l10n_update_source_download($source)) {
- if (l10n_update_source_import($source, $mode)) {
- l10n_update_source_history($source);
- return TRUE;
- }
- }
-}
-
-/**
- * Import source into locales table.
- *
- * @param $source
- * Translation source object with information about the file location.
- * Object will be updated with :
- * - 'last_checked': Timestamp of current time;
- * - 'import_date': Timestamp of current time;
- * @param $mode
- * Download mode. How to treat exising and modified translations.
- * @return boolean
- * Result array on success, FALSE on failure.
- */
-function l10n_update_source_import($source, $mode) {
- if (!empty($source->uri) && $result = l10n_update_import_file($source->uri, $source->language, $mode)) {
- $source->last_checked = REQUEST_TIME;
-
- // We override the file timestamp here. The default file time stamp is the
- // release date from the l.d.o server. We change the timestamp to the
- // creation time on the webserver. On multi sites that share a common
- // sites/all/translations directory, the sharing sites use the local file
- // creation date as release date. Without this correction the local
- // file is always newer than the l.d.o. file, which results in unnecessary
- // translation import.
- $source->timestamp = time();
-
- return $result;
- }
-}
-
-/**
- * Download source file from remote server.
- *
- * If succesful this function returns the downloaded file in two ways:
- * - As a temporary $file object
- * - As a file path on the $source->uri property.
- *
- * @param $source
- * Source object with all parameters
- * - 'fileurl': url to download.
- * - 'uri': alternate destination. If not present a temporary file
- * will be used and the path returned here.
- * @return object
- * $file object if download successful.
- */
-function l10n_update_source_download($source) {
- if (!empty($source->uri)) {
- $destination = $source->uri;
- }
- elseif ($directory = variable_get('l10n_update_download_store', '')) {
- $destination = $directory . '/' . $source->filename;
- }
- else {
- $destination = NULL;
- }
- if ($file = l10n_update_download_file($source->fileurl, $destination)) {
- $source->uri = $file;
- return $file;
- }
-}
-
-/**
- * Update the file history table and delete the file if temporary.
- *
- * @param $file
- * Source object representing the file just imported or downloaded.
- */
-function l10n_update_source_history($file) {
- // Update history table
- l10n_update_file_history($file);
-
- // If it's a downloaded file and not marked for keeping, delete the file.
- if ($file->type == 'download' && empty($file->keep)) {
- file_unmanaged_delete($file->uri);
- $file->uri = '';
- }
-}
-
-/**
- * Compare two update sources, looking for the newer one (bigger timestamp).
- *
- * This function can be used as a callback to compare two source objects.
- *
- * @param $current
- * Source object of current project.
- * @param $update
- * Source object of available update.
- * @return integer
- * - '-1': $current < $update OR $current is missing
- * - '0': $current == $update OR both $current and $updated are missing
- * - '1': $current > $update OR $update is missing
- */
-function _l10n_update_source_compare($current, $update) {
- if ($current && $update) {
- if (abs($current->timestamp - $update->timestamp) < L10N_UPDATE_TIMESTAMP_THRESHOLD) {
- return 0;
- }
- else {
- return $current->timestamp > $update->timestamp ? 1 : -1;
- }
- }
- elseif ($current && !$update) {
- return 1;
- }
- elseif (!$current && $update) {
- return -1;
- }
- else {
- return 0;
- }
-}
-
-/**
- * Prepare update list.
- *
- * @param $updates
- * Array of update sources that may be indexed in multiple ways.
- * @param $projects
- * Array of project names to be included, others will be filtered out.
- * @param $languages
- * Array of language codes to be included, others will be filtered out.
- * @return array
- * Plain array of filtered updates with directory applied.
- */
-function _l10n_update_prepare_updates($updates, $projects = NULL, $languages = NULL) {
- $result = array();
- foreach ($updates as $key => $update) {
- if (is_array($update)) {
- // It is a sub array of updates yet, process and merge
- $result = array_merge($result, _l10n_update_prepare_updates($update, $projects, $languages));
- }
- elseif ((!$projects || in_array($update->project, $projects)) && (!$languages || in_array($update->language, $languages))) {
- $directory = variable_get('l10n_update_download_store', '');
- if ($directory && empty($update->uri)) {
- // If we have a destination folder set just if we have no uri
- if (empty($update->uri)) {
- $update->uri = $directory . '/' . $update->filename;
- $update->keep = TRUE;
- }
- }
- $result[] = $update;
- }
- }
- return $result;
-}
-
-/**
- * Language refresh. Runs a batch for loading the selected languages.
- *
- * To be used after adding a new language.
- *
- * @param $languages
- * Array of language codes to check and download.
- */
-function l10n_update_language_refresh($languages) {
- $projects = l10n_update_get_projects();
- if ($available = l10n_update_check_projects($projects, $languages)) {
- $history = l10n_update_get_history();
- if ($updates = l10n_update_build_updates($history, $available)) {
- module_load_include('batch.inc', 'l10n_update');
- // Filter out updates in other languages. If no languages, all of them will be updated
- $updates = _l10n_update_prepare_updates($updates);
- $batch = l10n_update_batch_multiple($updates, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
- batch_set($batch);
- }
- }
-}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.compare.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.compare.inc
new file mode 100644
index 00000000..b5bb94b6
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.compare.inc
@@ -0,0 +1,386 @@
+execute();
+ drupal_static_reset('l10n_update_build_projects');
+}
+
+/**
+ * Rebuild project list
+ *
+ * @param $refresh
+ * TRUE: Refresh project list.
+ *
+ * @return array
+ * Array of project objects to be considered for translation update.
+ */
+function l10n_update_build_projects($refresh = FALSE) {
+ $projects = &drupal_static(__FUNCTION__, array(), $refresh);
+ if (empty($projects)) {
+ module_load_include('inc', 'l10n_update');
+
+ // Get the project list based on .info files.
+ $projects = l10n_update_project_list();
+
+ // Mark all previous projects as disabled and store new project data.
+ db_update('l10n_update_project')
+ ->fields(array(
+ 'status' => 0,
+ ))
+ ->execute();
+
+ $default_server = l10n_update_default_translation_server();
+
+ if (module_exists('update')) {
+ $projects_info = update_get_available(TRUE);
+ }
+ foreach ($projects as $name => $data) {
+
+ // Force update fetch of project data in cases where Drupal's performance
+ // optimized approach is missing out on some projects.
+ // @see http://drupal.org/node/1671570#comment-6216090
+ if (module_exists('update') && !isset($projects_info[$name])) {
+ module_load_include('fetch.inc', 'update');
+ _update_process_fetch_task($data);
+ $available = _update_get_cached_available_releases();
+ if (!empty($available[$name])) {
+ $projects_info[$name] = $available[$name];
+ }
+ }
+
+ if (isset($projects_info[$name]['releases']) && $projects_info[$name]['project_status'] != 'not-fetched') {
+ // Find out if a dev version is installed.
+ if (preg_match("/^[0-9]+\.x-([0-9]+)\..*-dev$/", $data['info']['version'], $matches)) {
+ // Find a suitable release to use as alternative translation.
+ foreach ($projects_info[$name]['releases'] as $project_release) {
+ // The first release with the same major release number which is not
+ // a dev release is the one. Releases are sorted the most recent first.
+ if ($project_release['version_major'] == $matches[1] &&
+ (!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
+ $release = $project_release;
+ break;
+ }
+ }
+ }
+ if (!empty($release['version'])) {
+ $data['info']['version'] = $release['version'];
+ }
+
+ unset($release);
+ }
+ // Without Update module we do a best effort fallback. A development
+ // release will fall back to the corresponding release version.
+ elseif (!isset($projects_info) && isset($data['info']['version'])) {
+ if (preg_match('/[^x](\+\d+)?-dev$/', $data['info']['version'])) {
+ $data['info']['version'] = preg_replace('/(\+\d+)?-dev$/', '', $data['info']['version']);
+ }
+ }
+
+ $data += array(
+ 'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
+ 'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
+ 'l10n_path' => isset($data['info']['l10n path']) && $data['info']['l10n path'] ? $data['info']['l10n path'] : $default_server['pattern'],
+ 'status' => 1,
+ );
+ $project = (object) $data;
+ $projects[$name] = $project;
+
+ // Create or update the project record.
+ db_merge('l10n_update_project')
+ ->key(array('name' => $project->name))
+ ->fields(array(
+ 'name' => $project->name,
+ 'project_type' => $project->project_type,
+ 'core' => $project->core,
+ 'version' => $project->version,
+ 'l10n_path' => $project->l10n_path,
+ 'status' => $project->status,
+ ))
+ ->execute();
+
+ // Invalidate the cache of translatable projects.
+ l10n_update_clear_cache_projects();
+ }
+ }
+ return $projects;
+}
+
+/**
+ * Get update module's project list
+ *
+ * @return array
+ */
+function l10n_update_project_list() {
+ $projects = array();
+ $disabled = variable_get('l10n_update_check_disabled', 0);
+ // Unlike update module, this one has no cache
+ _l10n_update_project_info_list($projects, system_rebuild_module_data(), 'module', $disabled);
+ _l10n_update_project_info_list($projects, system_rebuild_theme_data(), 'theme', $disabled);
+ // Allow other modules to alter projects before fetching and comparing.
+ drupal_alter('l10n_update_projects', $projects);
+ return $projects;
+}
+
+/**
+ * Populate an array of project data.
+ *
+ * Based on _update_process_info_list()
+ *
+ * @param $projects
+ * @param $list
+ * @param $project_type
+ * @param $disabled
+ * TRUE to include disabled projects too
+ */
+function _l10n_update_project_info_list(&$projects, $list, $project_type, $disabled = FALSE) {
+ foreach ($list as $file) {
+ if (!$disabled && empty($file->status)) {
+ // Skip disabled modules or themes.
+ continue;
+ }
+
+ // Skip if the .info file is broken.
+ if (empty($file->info)) {
+ continue;
+ }
+
+ // If the .info doesn't define the 'project', try to figure it out.
+ if (!isset($file->info['project'])) {
+ $file->info['project'] = l10n_update_get_project_name($file);
+ }
+
+ // If the .info defines the 'interface translation project', this value will
+ // override the 'project' value.
+ if (isset($file->info['interface translation project'])) {
+ $file->info['project'] = $file->info['interface translation project'];
+ }
+
+ // If we still don't know the 'project', give up.
+ if (empty($file->info['project'])) {
+ continue;
+ }
+
+ // If we don't already know it, grab the change time on the .info file
+ // itself. Note: we need to use the ctime, not the mtime (modification
+ // time) since many (all?) tar implementations will go out of their way to
+ // set the mtime on the files it creates to the timestamps recorded in the
+ // tarball. We want to see the last time the file was changed on disk,
+ // which is left alone by tar and correctly set to the time the .info file
+ // was unpacked.
+ if (!isset($file->info['_info_file_ctime'])) {
+ $info_filename = dirname($file->uri) . '/' . $file->name . '.info';
+ $file->info['_info_file_ctime'] = filectime($info_filename);
+ }
+
+ $project_name = $file->info['project'];
+ if (!isset($projects[$project_name])) {
+ // Only process this if we haven't done this project, since a single
+ // project can have multiple modules or themes.
+ $projects[$project_name] = array(
+ 'name' => $project_name,
+ 'info' => $file->info,
+ 'datestamp' => isset($file->info['datestamp']) ? $file->info['datestamp'] : 0,
+ 'includes' => array($file->name => isset($file->info['name']) ? $file->info['name'] : $file->name),
+ 'project_type' => $project_name == 'drupal' ? 'core' : $project_type,
+ );
+ }
+ else {
+ $projects[$project_name]['includes'][$file->name] = $file->info['name'];
+ $projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
+ }
+ }
+}
+
+/**
+ * Given a $file object (as returned by system_rebuild_module_data()), figure
+ * out what project it belongs to.
+ *
+ * Based on update_get_project_name().
+ *
+ * @param $file
+ * @return string
+ * @see system_get_files_database()
+ */
+function l10n_update_get_project_name($file) {
+ $project_name = '';
+ if (isset($file->info['project'])) {
+ $project_name = $file->info['project'];
+ }
+ elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
+ $project_name = 'drupal';
+ }
+ return $project_name;
+}
+
+/**
+ * Retrieve data for default server.
+ *
+ * @return array
+ * Array of server parameters:
+ * - "server_pattern": URI containing po file pattern.
+ */
+function l10n_update_default_translation_server() {
+ $pattern = variable_get('l10n_update_default_update_url', L10N_UPDATE_DEFAULT_SERVER_PATTERN);
+
+ return array(
+ 'pattern' => $pattern,
+ );
+}
+
+/**
+ * Check for the latest release of project translations.
+ *
+ * @param array $projects
+ * Array of project names to check. Defaults to all translatable projects.
+ * @param string $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ * Available sources indexed by project and language.
+ */
+// @todo Return batch or NULL
+function l10n_update_check_projects($projects = array(), $langcodes = array()) {
+ if (l10n_update_use_remote_source()) {
+ // Retrieve the status of both remote and local translation sources by
+ // using a batch process.
+ l10n_update_check_projects_batch($projects, $langcodes);
+ }
+ else {
+ // Retrieve and save the status of local translations only.
+ l10n_update_check_projects_local($projects, $langcodes);
+ variable_set('l10n_update_last_check', REQUEST_TIME);
+ }
+}
+
+/**
+ * Gets and stores the status and timestamp of remote po files.
+ *
+ * A batch process is used to check for po files at remote locations and (when
+ * configured) to check for po files in the local file system. The most recent
+ * translation source states are stored in the state variable
+ * 'l10n_update_translation_status'.
+ *
+ * @param array $projects
+ * Array of project names to check. Defaults to all translatable projects.
+ * @param string $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ */
+function l10n_update_check_projects_batch($projects = array(), $langcodes = array()) {
+ // Build and set the batch process.
+ $batch = l10n_update_batch_status_build($projects, $langcodes);
+ batch_set($batch);
+}
+
+/**
+ * Builds a batch to get the status of remote and local translation files.
+ *
+ * The batch process fetches the state of both local and (if configured) remote
+ * translation files. The data of the most recent translation is stored per
+ * per project and per language. This data is stored in a state variable
+ * 'l10n_update_translation_status'. The timestamp it was last updated is stored
+ * in the state variable 'l10n_upate_last_checked'.
+ *
+ * @param array $projects
+ * Array of project names for which to check the state of translation files.
+ * Defaults to all translatable projects.
+ * @param array $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ * Batch definition array.
+ */
+function l10n_update_batch_status_build($projects = array(), $langcodes = array()) {
+ $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+ $options = _l10n_update_default_update_options();
+
+ $operations = _l10n_update_batch_status_operations($projects, $langcodes, $options);
+
+ $batch = array(
+ 'operations' => $operations,
+ 'title' => t('Checking translations'),
+ 'progress_message' => '',
+ 'finished' => 'l10n_update_batch_status_finished',
+ 'error_message' => t('Error checking translation updates.'),
+ 'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
+ );
+ return $batch;
+}
+
+/**
+ * Helper function to construct batch operations checking remote translation
+ * status.
+ *
+ * @param array $projects
+ * Array of project names to be processed.
+ * @param array $langcodes
+ * Array of language codes.
+ * @param array $options
+ * Batch processing options.
+ *
+ * @return array
+ * Array of batch operations.
+ */
+function _l10n_update_batch_status_operations($projects, $langcodes, $options = array()) {
+ $operations = array();
+
+ foreach ($projects as $project) {
+ foreach ($langcodes as $langcode) {
+ // Check status of local and remote translation sources.
+ $operations[] = array('l10n_update_batch_status_check', array($project, $langcode, $options));
+ }
+ }
+
+ return $operations;
+}
+
+/**
+ * Check and store the status and timestamp of local po files.
+ *
+ * Only po files in the local file system are checked. Any remote translation
+ * files will be ignored.
+ *
+ * Projects may contain a server_pattern option containing a pattern of the
+ * path to the po source files. If no server_pattern is defined the default
+ * translation directory is checked for the po file. When a server_pattern is
+ * defined the specified location is checked. The server_pattern can be set in
+ * the module's .info.yml file or by using
+ * hook_l10n_update_projects_alter().
+ *
+ * @param array $projects
+ * Array of project names for which to check the state of translation files.
+ * Defaults to all translatable projects.
+ * @param array $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ */
+function l10n_update_check_projects_local($projects = array(), $langcodes = array()) {
+ $projects = l10n_update_get_projects($projects);
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+
+ // For each project and each language we check if a local po file is
+ // available. When found the source object is updated with the appropriate
+ // type and timestamp of the po file.
+ foreach ($projects as $name => $project) {
+ foreach ($langcodes as $langcode) {
+ $source = l10n_update_source_build($project, $langcode);
+ if ($file = l10n_update_source_check_file($source)) {
+ l10n_update_status_save($name, $langcode, L10N_UPDATE_LOCAL, $file);
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.drush.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.drush.inc
index 087b60bd..3f06cfce 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.drush.inc
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.drush.inc
@@ -16,14 +16,14 @@ function l10n_update_drush_command() {
$commands['l10n-update-status'] = array(
'description' => 'Show translation status of available projects.',
'options' => array(
- 'languages' => 'Comma separated list of languages. Defaults to all available languages.',
+ 'languages' => 'Comma separated list of languages. Defaults to all available languages. Example: --languages="nl, fr, de"',
)
);
$commands['l10n-update'] = array(
'description' => 'Update translations.',
'options' => array(
- 'languages' => 'Comma separated list of languages. Defaults to all available languages.',
- 'mode' => 'Allowed values: overwrite, keep. Default value: keep.'
+ 'languages' => 'Comma separated list of languages. Defaults to all available languages. Example: --languages="nl, fr, de"',
+ 'mode' => 'Determine if existing translations are overwitten during import. Use "overwrite" to overwrite any existing translation, "replace" to replace previously imported translations but not overwrite edited strings, "keep" to keep any existing translation and only add new translations. Default value: "keep"'
),
);
return $commands;
@@ -33,56 +33,66 @@ function l10n_update_drush_command() {
* Callback for command l10n-update-refresh.
*/
function drush_l10n_update_refresh() {
- module_load_include('admin.inc', 'l10n_update');
+ module_load_include('compare.inc', 'l10n_update');
- // Fake $form_state to leverage _submit function.
- $form_state = array(
- 'values' => array('op' => t('Refresh information'))
- );
- l10n_update_admin_import_form_submit(NULL, $form_state);
+ // Check the translation status of all translatable projects in all languages.
+ // First we clear the cached list of projects. Although not strictly
+ // necessary, this is helpful in case the project list is out of sync.
+ l10n_update_flush_projects();
+ l10n_update_check_projects();
+
+ // Execute a batch if required. A batch is only used when remote files
+ // are checked.
+ if (batch_get()) {
+ drush_backend_batch_process();
+ }
}
/**
* Validate command l10n-update-status.
*/
function drush_l10n_update_status_validate() {
- _drush_l10n_update_validate_languages();
+ return _drush_l10n_update_validate_languages();
}
/**
* Callback for command l10n-update-status.
*/
function drush_l10n_update_status() {
- $updates = _drush_l10n_update_get_updates();
- if (!is_null($updates)) {
+ $status = l10n_update_get_status();
+
+ if (!empty($status)) {
$languages = drush_get_option('languages');
- // Table header.
+
+ // Build table header.
$table = array();
$header = array(dt('Project'));
- foreach ($languages as $lang => $language) {
- $header[] = $language . ' status';
+ foreach ($languages as $langcode => $language) {
+ $header[] = $language->name;
}
$table[] = $header;
+
// Iterate projects to obtain per language status.
- $projects = l10n_update_get_projects();
- $history = l10n_update_get_history();
- foreach ($projects as $name => $project) {
+ foreach ($status as $name => $project) {
$row = array();
- // Project.
- $title = isset($project->title) ? $project->title : $project->name;
- $row[] = $title . ' ' . $project->version;
- // Language status.
- foreach ($languages as $lang => $language) {
- $current = isset($history[$name][$lang]) ? $history[$name][$lang] : NULL;
- $update = isset($updates[$name][$lang]) ? $updates[$name][$lang] : NULL;
- if ($update) {
- $row[] = ($update->type == 'download') ? t('Remote update available'):t('Local update available');
+ // First column: Project name & version number.
+ $project_details = reset($project);
+ $title = isset($project_details->title) ? $project_details->title : $project_details->name;
+ $row[] = dt('@project (@version)', array('@project' => $title, '@version' => $project_details->version));
+
+ // Other columns: Status per language.
+ foreach ($languages as $langcode => $language) {
+ $current = $project[$langcode]->type == L10N_UPDATE_CURRENT;
+ $local_update = $project[$langcode]->type == L10N_UPDATE_LOCAL;
+ $remote_update = $project[$langcode]->type == L10N_UPDATE_REMOTE;
+ if ($local_update || $remote_update) {
+ $row[] = $remote_update ? dt('Remote update available') : dt('Local update available');
}
elseif ($current) {
- $row[] = t('Up to date');
+ $row[] = dt('Up to date');
}
else {
- $row[] = t('No information');
+ $row[] = dt('No info');
}
}
$table[] = $row;
@@ -90,7 +100,7 @@ function drush_l10n_update_status() {
drush_print_table($table, TRUE);
}
else {
- drush_log(dt('No projects or languages to update.'), 'ok');
+ drush_log(dt('No languages to update.'), 'warning');
}
}
@@ -98,12 +108,14 @@ function drush_l10n_update_status() {
* Validate command l10n-update.
*/
function drush_l10n_update_validate() {
- _drush_l10n_update_validate_languages();
-
+ $lang_validation = _drush_l10n_update_validate_languages();
+ if ($lang_validation == FALSE) {
+ return FALSE;
+ }
// Check provided update mode is valid.
$mode = drush_get_option('mode', 'keep');
- if (!in_array($mode, array('keep', 'overwrite'))) {
- return drush_set_error('L10N_UPDATE_INVALID_MODE', dt('Invalid update mode. Valid options are keep, overwrite.'));
+ if (!in_array($mode, array('keep', 'replace', 'overwrite'))) {
+ return drush_set_error('L10N_UPDATE_INVALID_MODE', dt('Invalid update mode. Valid options are keep, replace, overwrite.'));
}
}
@@ -111,25 +123,52 @@ function drush_l10n_update_validate() {
* Callback for command l10n-update.
*/
function drush_l10n_update() {
+ module_load_include('fetch.inc', 'l10n_update');
$updates = _drush_l10n_update_get_updates();
- if (!is_null($updates)) {
- if (count($updates) > 0) {
- drush_log(dt('Found @count projects to update.', array('@count' => count($updates))), 'status');
- // Batch update all projects for selected languages.
- $mode = drush_get_option('mode', 'keep');
- $languages = drush_get_option('languages');
- module_load_include('batch.inc', 'l10n_update');
- $updates = _l10n_update_prepare_updates($updates, NULL, array_keys($languages));
- $batch = l10n_update_batch_multiple($updates, $mode);
- drush_log($batch['title'], 'status');
- drush_log($batch['init_message'], 'status');
- batch_set($batch);
- drush_backend_batch_process();
- }
- else {
- drush_log(dt('All translations up to date'), 'status');
+ if ($updates['projects']) {
+ drush_log(dt('Found @count projects to update.', array('@count' => count($updates['projects']))), 'status');
+
+ // Batch update all projects for selected languages.
+ $mode = drush_get_option('mode', 'keep');
+ $options = _l10n_update_default_update_options();
+
+ switch ($mode) {
+ case 'keep':
+ $options['overwrite_options'] = array(
+ 'not_customized' => FALSE,
+ 'customized' => FALSE,
+ );
+ break;
+ case 'replace':
+ $options['overwrite_options'] = array(
+ 'not_customized' => TRUE,
+ 'customized' => FALSE,
+ );
+ break;
+ case 'overwrite':
+ $options['overwrite_options'] = array(
+ 'not_customized' => TRUE,
+ 'customized' => TRUE,
+ );
+ break;
+
+ default:
+ return drush_set_error('L10N_UPDATE_INVALID_MODE', dt('Invalid update mode. Valid options are keep, overwrite.'));
+ break;
}
+
+ $languages = array_keys(drush_get_option('languages'));
+
+ // Get translation status of the projects, download and update translations.
+ $batch = l10n_update_batch_update_build(array(), $languages, $options);
+ drush_log($batch['title'], 'status');
+ drush_log($batch['init_message'], 'status');
+ batch_set($batch);
+ drush_backend_batch_process();
+ }
+ else {
+ drush_log(dt('All project translations up to date'), 'status');
}
}
@@ -141,32 +180,43 @@ function drush_l10n_update() {
* 2. Check user provided languages are valid.
*/
function _drush_l10n_update_validate_languages() {
- // Check there're installed other languages than english.
- $installed_languages = l10n_update_language_list();
+ // Check if there are installed languages other than english.
+ $installed_languages = l10n_update_translatable_language_list();
+
+ // Indicate that there's nothing to do, only show a warning.
if (empty($installed_languages)) {
- return drush_set_error('L10N_UPDATE_NO_LANGUAGES', dt('No languages to update.'));
+ drush_log(dt('No languages to update.'), 'warning');
+ return FALSE;
}
+
// Check provided languages are valid.
$languages = drush_get_option('languages', '');
$languages = array_map('trim', _convert_csv_to_array($languages));
if (count($languages)) {
- foreach ($languages as $key => $lang) {
- if (!isset($installed_languages[$lang])) {
- drush_set_error('L10N_UPDATE_INVALID_LANGUAGE', dt('Language @lang is not installed.', array('@lang' => $lang)));
+ foreach ($languages as $key => $langcode) {
+ if (!isset($installed_languages[$langcode])) {
+ if (is_numeric($langcode)) {
+ drush_set_error('L10N_UPDATE_INVALID_LANGUAGE', dt('Invalid language "@langcode". Use for example: --languages="nl, fr, de"', array('@langcode' => $langcode)));
+ }
+ else {
+ drush_set_error('L10N_UPDATE_INVALID_LANGUAGE', dt('Language "@langcode" is not installed.', array('@langcode' => $langcode)));
+ }
}
else {
unset($languages[$key]);
- $languages[$lang] = $installed_languages[$lang];
+ $languages[$langcode] = $installed_languages[$langcode];
}
}
if (drush_get_error() != DRUSH_SUCCESS) {
drush_print(dt('Available languages: @languages', array('@languages' => implode(', ', array_keys($installed_languages)))));
+ return FALSE;
}
}
else {
$languages = $installed_languages;
}
drush_set_option('languages', $languages);
+ return TRUE;
}
/**
@@ -175,16 +225,29 @@ function _drush_l10n_update_validate_languages() {
* @return $updates array or NULL.
*/
function _drush_l10n_update_get_updates() {
- $projects = l10n_update_get_projects();
- if ($projects) {
- $history = l10n_update_get_history();
+ $updates = array();
+ $languages = l10n_update_translatable_language_list();
+ $status = l10n_update_get_status();
+
drush_log(dt('Fetching update information for all projects / all languages.'), 'status');
- module_load_include('check.inc', 'l10n_update');
- $available = l10n_update_available_releases();
- $updates = l10n_update_build_updates($history, $available);
+
+ // Prepare information about projects which have available translation
+ // updates.
+ if ($languages && $status) {
+ foreach ($status as $project) {
+ foreach ($project as $langcode => $project_info) {
+ // Translation update found for this project-language combination.
+ if ($project_info->type && ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE)) {
+ $updates['projects'][$project_info->name] = $project_info;
+ $updates['languages'][$langcode] = $project_info;
+ }
+ }
+ }
+ }
+ if ($updates) {
return $updates;
}
else {
- drush_log(dt('No projects or languages to update.'), 'ok');
+ drush_log(dt('No languages to update.'), 'warning');
}
}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.fetch.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.fetch.inc
new file mode 100644
index 00000000..85178b2a
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.fetch.inc
@@ -0,0 +1,108 @@
+ $operations,
+ 'title' => t('Updating translations'),
+ 'progress_message' => '',
+ 'error_message' => t('Error importing translation files'),
+ 'finished' => 'l10n_update_batch_fetch_finished',
+ 'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
+ );
+ return $batch;
+}
+
+/**
+ * Builds a batch to download and import project translations.
+ *
+ * @param array $projects
+ * Array of project names for which to check the state of translation files.
+ * Defaults to all translatable projects.
+ * @param array $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ * @param array $options
+ * Array of import options. See l10n_update_batch_import_files().
+ *
+ * @return array
+ * Batch definition array.
+ */
+function l10n_update_batch_fetch_build($projects = array(), $langcodes = array(), $options = array()) {
+ $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+
+ $batch = array(
+ 'operations' => _l10n_update_fetch_operations($projects, $langcodes, $options),
+ 'title' => t('Updating translations.'),
+ 'progress_message' => '',
+ 'error_message' => t('Error importing translation files'),
+ 'finished' => 'l10n_update_batch_fetch_finished',
+ 'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
+ );
+ return $batch;
+}
+
+/**
+ * Helper function to construct the batch operations to fetch translations.
+ *
+ * @param array $projects
+ * Array of project names for which to check the state of translation files.
+ * Defaults to all translatable projects.
+ * @param array $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ * @param array $options
+ * Array of import options.
+ *
+ * @return array
+ * Array of batch operations.
+ */
+function _l10n_update_fetch_operations($projects, $langcodes, $options) {
+ $operations = array();
+
+ foreach ($projects as $project) {
+ foreach ($langcodes as $langcode) {
+ if (l10n_update_use_remote_source()) {
+ $operations[] = array('l10n_update_batch_fetch_download', array($project, $langcode));
+ }
+ $operations[] = array('l10n_update_batch_fetch_import', array($project, $langcode, $options));
+ }
+ }
+
+ return $operations;
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.http.inc
similarity index 54%
rename from sites/all/modules/contrib/localisation/l10n_update/l10n_update.inc
rename to sites/all/modules/contrib/localisation/l10n_update/l10n_update.http.inc
index 9ab89080..0f05ae5f 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.inc
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.http.inc
@@ -2,248 +2,9 @@
/**
* @file
- * Reusable API for l10n remote updates.
+ * Http API for l10n updates.
*/
-include_once DRUPAL_ROOT . '/includes/locale.inc';
-module_load_include('locale.inc', 'l10n_update');
-
-/**
- * Default update server, filename and URL.
- */
-define('L10N_UPDATE_DEFAULT_SERVER', 'localize.drupal.org');
-define('L10N_UPDATE_DEFAULT_SERVER_URL', 'http://localize.drupal.org/l10n_server.xml');
-define('L10N_UPDATE_DEFAULT_UPDATE_URL', 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po');
-
-// Translation filename, will be used just for local imports
-define('L10N_UPDATE_DEFAULT_FILENAME', '%project-%release.%language.po');
-
-// Translation status: String imported from po
-define('L10N_UPDATE_STRING_DEFAULT', 0);
-
-// Translation status: Custom string, overridden original import
-define('L10N_UPDATE_STRING_CUSTOM', 1);
-
-/**
- * Retrieve data for default server.
- *
- * @return array
- * Server parameters:
- * name : Localization server name
- * server_url : Localization server URL where language list can be retrieved.
- * update_url : URL containing po file pattern.
- */
-function l10n_update_default_server() {
- return array(
- 'name' => variable_get('l10n_update_default_server', L10N_UPDATE_DEFAULT_SERVER),
- 'server_url' => variable_get('l10n_update_default_server_url', L10N_UPDATE_DEFAULT_SERVER_URL),
- 'update_url' => variable_get('l10n_update_default_update_url', L10N_UPDATE_DEFAULT_UPDATE_URL),
- );
-}
-
-/**
- * Download and import remote translation file.
- *
- * @param $download_url
- * Download URL.
- * @param $locale
- * Language code.
- * @param $mode
- * Download mode. How to treat exising and modified translations.
- *
- * @return boolean
- * TRUE on success.
- */
-function l10n_update_download_import($download_url, $locale, $mode = LOCALE_IMPORT_OVERWRITE) {
- if ($file = l10n_update_download_file($download_url)) {
- $result = l10n_update_import_file($file, $locale, $mode);
- return $result;
- }
-}
-
-/**
- * Import local file into the database.
- *
- * @param $file
- * File object of localy stored file
- * or path to localy stored file.
- * @param $locale
- * Language code.
- * @param $mode
- * Download mode. How to treat exising and modified translations.
- *
- * @return boolean
- * Result array on success. FALSE on failure
- */
-function l10n_update_import_file($file, $locale, $mode = LOCALE_IMPORT_OVERWRITE) {
- // If the file is a uri, create a $file object
- if (is_string($file)) {
- $uri = $file;
- $file = new stdClass();
- $file->uri = $uri;
- $file->filename = $uri;
- }
- return _l10n_update_locale_import_po($file, $locale, $mode, 'default');
-}
-
-/**
- * Get remote file and download it to a temporary path.
- *
- * @param $download_url
- * URL of remote file.
- * @param $destination
- * URL of local destination file. By default the download will be stored
- * in a temporary file.
- */
-function l10n_update_download_file($download_url, $destination = NULL) {
- $t = get_t();
- $variables['%download_link'] = $download_url;
-
- // Create temporary file or use specified file destination.
- // Temporary files get a 'translation-' file name prefix.
- $file = $destination ? $destination : drupal_tempnam(file_directory_temp(), 'translation-');
-
- if ($file) {
- $variables['%tmpfile'] = $file;
- // We download and store the file (in one if statement! Isnt't that neat ;) ).
- // @todo remove the timeout once we use the batch API to download the files.
- if (($contents = drupal_http_request($download_url, array('timeout' => 90))) && $contents->code == 200 && $file_result = file_put_contents($file, $contents->data)) {
- watchdog('l10n_update', 'Successfully downloaded %download_link to %tmpfile', $variables);
- return $file;
- }
- else {
- if (isset($contents->error)) {
- watchdog('l10n_update', 'An error occured during the download operation: %error.', array('%error' => $contents->error), WATCHDOG_ERROR);
- }
- elseif (isset($contents->code) && $contents->code != 200) {
- watchdog('l10n_update', 'An error occured during the download operation: HTTP status code %code.', array('%code' => $contents->code), WATCHDOG_ERROR);
- }
- if (isset($file_result)) {
- // file_put_contents() was called but returned FALSE.
- watchdog('l10n_update', 'Unable to save %download_link file to %tmpfile.', $variables, WATCHDOG_ERROR);
- }
- }
-
- }
- else {
- $variables['%tmpdir'] = file_directory_temp();
- watchdog('l10n_update', 'Error creating temporary file for download in %tmpdir. Remote file is %download_link.', $variables, WATCHDOG_ERROR);
- }
-}
-
-/**
- * Get names for the language list from locale system.
- *
- * @param $string_list
- * Comma separated list of language codes.
- * Language codes must exist in languages from _locale_get_predefined_list().
- * @return array
- * Array of language names keyed by language code.
- */
-function l10n_update_get_language_names($string_list) {
- $t = get_t();
- $language_codes = array_map('trim', explode(',', $string_list));
- $languages = _locale_get_predefined_list();
- $result = array();
- foreach ($language_codes as $lang) {
- if (array_key_exists($lang, $languages)) {
- // Try to use verbose locale name
- $name = $lang;
- $name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . $t('(@language)', array('@language' => $languages[$name][1])) : '');
- $result[$lang] = $name;
- }
- }
- return $result;
-}
-
-/**
- * Build project data as an object.
- *
- * @param $name
- * Project name.
- * @param $version
- * Project version.
- * @param $server
- * Localisation server name.
- * @param $path
- * Localisation server URL.
- * @return object
- * Project object containing the supplied data.
- */
-function _l10n_update_build_project($name, $version = NULL, $server = L10N_UPDATE_DEFAULT_SERVER, $path = L10N_UPDATE_DEFAULT_SERVER_URL) {
- $project = new stdClass();
- $project->name = $name;
- $project->version = $version;
- $project->l10n_server = $server;
- $project->l10n_path = $path;
- return $project;
-}
-
-/**
- * Update the file history table.
- *
- * @param $file
- * Object representing the file just imported or downloaded.
- * @return integer
- * FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
- * @see drupal_write_record()
- */
-function l10n_update_file_history($file) {
- // Update or write new record
- if (db_query("SELECT project FROM {l10n_update_file} WHERE project = :project AND language = :language", array(':project' => $file->project, ':language' => $file->language))->fetchField()) {
- $update = array('project', 'language');
- }
- else {
- $update = array();
- }
- return drupal_write_record('l10n_update_file', $file, $update);
-}
-
-/**
- * Delete the history of downloaded translations.
- *
- * @param string $langcode
- * Language code of the file history to be deleted.
- */
-function l10n_update_delete_file_history($langcode) {
- db_delete('l10n_update_file')
- ->condition('language', $langcode)
- ->execute();
-}
-
-/**
- * Flag the file history as up to date.
- *
- * Compare history data in the {l10n_update_file} table with translations
- * available at translations server(s). Update the 'last_checked' timestamp of
- * the files which are up to date.
- *
- * @param $available
- * Available translations as retreived from remote server.
- */
-function l10n_update_flag_history($available) {
- if ($history = l10n_update_get_history()) {
- foreach($history as $name => $project) {
- foreach ($project as $langcode => $current) {
- if (isset($available[$name][$langcode])) {
- $update = $available[$name][$langcode];
- // When the available update is equal to the current translation the current
- // is marked checked in the {l10n_update_file} table.
- if (_l10n_update_source_compare($current, $update) == 0 && $current->version == $update->version) {
- db_update('l10n_update_file')
- ->fields(array(
- 'last_checked' => REQUEST_TIME,
- ))
- ->condition('project', $current->project)
- ->condition('language', $current->language)
- ->execute();
- }
- }
- }
- }
- }
-}
-
/**
* Check if remote file exists and when it was last updated.
*
@@ -258,8 +19,29 @@ function l10n_update_flag_history($available) {
*/
function l10n_update_http_check($url, $headers = array()) {
$result = l10n_update_http_request($url, array('headers' => $headers, 'method' => 'HEAD'));
- if ($result && $result->code == '200') {
- $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
+ if (!isset($result->error)) {
+ if ($result && $result->code == 200) {
+ $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
+ }
+ return $result;
+ }
+ else {
+ switch ($result->code) {
+ case 404:
+ // File not found occurs when a translation file is not yet available
+ // at the translation server. But also if a custom module or custom
+ // theme does not define the location of a translation file. By default
+ // the file is checked at the translation server, but it will not be
+ // found there.
+ watchdog('l10n_update', 'File not found: @uri.', array('@uri' => $url));
+ return TRUE;
+ case 0:
+ watchdog('l10n_update', 'Error occurred when trying to check @remote: @errormessage.', array('@errormessage' => $result->error, '@remote' => $url), WATCHDOG_ERROR);
+ break;
+ default:
+ watchdog('l10n_update', 'HTTP error @errorcode occurred when trying to check @remote.', array('@errorcode' => $result->code, '@remote' => $url), WATCHDOG_ERROR);
+ break;
+ }
}
return $result;
}
@@ -297,7 +79,8 @@ function l10n_update_http_check($url, $headers = array()) {
* received.
* - redirect_code: If redirected, an integer containing the initial response
* status code.
- * - redirect_url: If redirected, a string containing the redirection location.
+ * - redirect_url: If redirected, a string containing the URL of the redirect
+ * target.
* - error: If an error occurred, the error message. Otherwise not set.
* - headers: An array containing the response headers as name/value pairs.
* HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
@@ -333,10 +116,51 @@ function l10n_update_http_request($url, array $options = array()) {
'timeout' => 30.0,
'context' => NULL,
);
+
+ // Merge the default headers.
+ $options['headers'] += array(
+ 'User-Agent' => 'Drupal (+http://drupal.org/)',
+ );
+
// stream_socket_client() requires timeout to be a float.
$options['timeout'] = (float) $options['timeout'];
+ // Use a proxy if one is defined and the host is not on the excluded list.
+ $proxy_server = variable_get('proxy_server', '');
+ if ($proxy_server && _drupal_http_use_proxy($uri['host'])) {
+ // Set the scheme so we open a socket to the proxy server.
+ $uri['scheme'] = 'proxy';
+ // Set the path to be the full URL.
+ $uri['path'] = $url;
+ // Since the URL is passed as the path, we won't use the parsed query.
+ unset($uri['query']);
+
+ // Add in username and password to Proxy-Authorization header if needed.
+ if ($proxy_username = variable_get('proxy_username', '')) {
+ $proxy_password = variable_get('proxy_password', '');
+ $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : ''));
+ }
+ // Some proxies reject requests with any User-Agent headers, while others
+ // require a specific one.
+ $proxy_user_agent = variable_get('proxy_user_agent', '');
+ // The default value matches neither condition.
+ if ($proxy_user_agent === NULL) {
+ unset($options['headers']['User-Agent']);
+ }
+ elseif ($proxy_user_agent) {
+ $options['headers']['User-Agent'] = $proxy_user_agent;
+ }
+ }
+
switch ($uri['scheme']) {
+ case 'proxy':
+ // Make the socket connection to a proxy server.
+ $socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080);
+ // The Host header still needs to match the real request.
+ $options['headers']['Host'] = $uri['host'];
+ $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
+ break;
+
case 'http':
case 'feed':
$port = isset($uri['port']) ? $uri['port'] : 80;
@@ -346,12 +170,14 @@ function l10n_update_http_request($url, array $options = array()) {
// checking the host that do not take into account the port number.
$options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
break;
+
case 'https':
// Note: Only works when PHP is compiled with OpenSSL support.
$port = isset($uri['port']) ? $uri['port'] : 443;
$socket = 'ssl://' . $uri['host'] . ':' . $port;
$options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
break;
+
default:
$result->error = 'invalid schema ' . $uri['scheme'];
$result->code = -1003;
@@ -376,7 +202,7 @@ function l10n_update_http_request($url, array $options = array()) {
// Mark that this request failed. This will trigger a check of the web
// server's ability to make outgoing HTTP requests the next time that
// requirements checking is performed.
- // See system_requirements()
+ // See system_requirements().
// variable_set('drupal_http_request_fails', TRUE);
return $result;
@@ -388,11 +214,6 @@ function l10n_update_http_request($url, array $options = array()) {
$path .= '?' . $uri['query'];
}
- // Merge the default headers.
- $options['headers'] += array(
- 'User-Agent' => 'Drupal (+http://drupal.org/)',
- );
-
// Only add Content-Length if we actually have any content or if it is a POST
// or PUT request. Some non-standard servers get confused by Content-Length in
// at least HEAD/GET requests, and Squid always requires Content-Length in
@@ -404,7 +225,7 @@ function l10n_update_http_request($url, array $options = array()) {
// If the server URL has a user then attempt to use basic authentication.
if (isset($uri['user'])) {
- $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : ''));
+ $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
}
// If the database prefix is being used by SimpleTest to run the tests in a copied
@@ -459,7 +280,9 @@ function l10n_update_http_request($url, array $options = array()) {
return $result;
}
// Parse response headers from the response body.
- list($response, $result->data) = explode("\r\n\r\n", $response, 2);
+ // Be tolerant of malformed HTTP responses that separate header and body with
+ // \n\n or \r\r instead of \r\n\r\n.
+ list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2);
$response = preg_split("/\r\n|\n|\r/", $response);
// Parse the response status line.
@@ -551,7 +374,9 @@ function l10n_update_http_request($url, array $options = array()) {
$result = l10n_update_http_request($location, $options);
$result->redirect_code = $code;
}
- $result->redirect_url = $location;
+ if (!isset($result->redirect_url)) {
+ $result->redirect_url = $location;
+ }
break;
default:
$result->error = $status_message;
@@ -559,29 +384,3 @@ function l10n_update_http_request($url, array $options = array()) {
return $result;
}
-
-/**
- * Build abstract translation source, to be mapped to a file or a download.
- *
- * @param $project
- * Project object containing data to be inserted in the template.
- * @param $template
- * String containing place holders. Available place holders:
- * - '%project': Project name.
- * - '%release': Poject version.
- * - '%core': Project core version.
- * - '%language': Language code.
- * - '%filename': Project file name.
- * @return string
- * String with replaced place holders.
- */
-function l10n_update_build_string($project, $template) {
- $variables = array(
- '%project' => $project->name,
- '%release' => $project->version,
- '%core' => $project->core,
- '%language' => isset($project->language) ? $project->language : '%language',
- '%filename' => isset($project->filename) ? $project->filename : '%filename',
- );
- return strtr($template, $variables);
-}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.info b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.info
index a2652945..6b6cb297 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.info
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.info
@@ -4,21 +4,35 @@ dependencies[] = locale
core = 7.x
package = Multilingual
-files[] = l10n_update.admin.inc
-files[] = l10n_update.api.php
-files[] = l10n_update.batch.inc
-files[] = l10n_update.check.inc
-files[] = l10n_update.drush.inc
-files[] = l10n_update.inc
-files[] = l10n_update.install
-files[] = l10n_update.locale.inc
-files[] = l10n_update.module
-files[] = l10n_update.parser.inc
-files[] = l10n_update.project.inc
+files[] = includes/gettext/PoHeader.php
+files[] = includes/gettext/PoItem.php
+files[] = includes/gettext/PoMemoryWriter.php
+files[] = includes/gettext/PoMetadataInterface.php
+files[] = includes/gettext/PoReaderInterface.php
+files[] = includes/gettext/PoStreamInterface.php
+files[] = includes/gettext/PoStreamReader.php
+files[] = includes/gettext/PoStreamWriter.php
+files[] = includes/gettext/PoWriterInterface.php
-; Information added by drupal.org packaging script on 2012-02-06
-version = "7.x-1.0-beta3"
+files[] = includes/locale/Gettext.php
+files[] = includes/locale/PoDatabaseReader.php
+files[] = includes/locale/PoDatabaseWriter.php
+files[] = includes/locale/SourceString.php
+files[] = includes/locale/StringBase.php
+files[] = includes/locale/StringDatabaseStorage.php
+files[] = includes/locale/StringInterface.php
+files[] = includes/locale/StringStorageException.php
+files[] = includes/locale/StringStorageInterface.php
+files[] = includes/locale/TranslationString.php
+files[] = includes/locale/TranslationsStreamWrapper.php
+
+files[] = tests/L10nUpdateCronTest.test
+files[] = tests/L10nUpdateInterfaceTest.test
+files[] = tests/L10nUpdateTest.test
+files[] = tests/L10nUpdateTestBase.test
+; Information added by Drupal.org packaging script on 2014-11-10
+version = "7.x-2.0"
core = "7.x"
project = "l10n_update"
-datestamp = "1328563848"
+datestamp = "1415625781"
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.install b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.install
index a26f972b..0b47d3eb 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.install
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.install
@@ -53,7 +53,7 @@ function l10n_update_schema() {
'default' => '',
),
'status' => array(
- 'description' => 'Status flag. TBD',
+ 'description' => 'Status flag. If TRUE, translations of this module will be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
@@ -150,6 +150,7 @@ function l10n_update_schema_alter(&$schema) {
'type' => 'int',
'not null' => TRUE,
'default' => 0,
+ 'description' => 'Boolean indicating whether the translation is custom to this site.',
);
}
@@ -159,6 +160,22 @@ function l10n_update_schema_alter(&$schema) {
function l10n_update_install() {
db_add_field('locales_target', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
variable_set('l10n_update_rebuild_projects', 1);
+
+ // Create the translation directory. We try different alternative paths as the
+ // default may not always be writable.
+ $directories = array(
+ variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
+ variable_get('file_public_path', conf_path() . '/files') . '/translations',
+ 'sites/default/files/translations',
+ );
+ foreach ($directories as $directory) {
+ if (file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+ variable_set('l10n_update_download_store', $directory);
+ return;
+ }
+ }
+ watchdog('l10n_update', 'The directory %directory does not exist or is not writable.', array('%directory' => $directories[0]), WATCHDOG_ERROR);
+ drupal_set_message(t('The directory %directory does not exist or is not writable.', array('%directory' => $directories[0])), 'error');
}
/**
@@ -168,77 +185,85 @@ function l10n_update_uninstall() {
db_drop_field('locales_target', 'l10n_status');
variable_del('l10n_update_check_disabled');
- variable_del('l10n_update_check_frequency');
- variable_del('l10n_update_check_mode');
- variable_del('l10n_update_default_server');
+ variable_del('l10n_update_default_filename');
variable_del('l10n_update_default_update_url');
- variable_del('l10n_update_download_store');
+ variable_del('l10n_update_import_enabled');
variable_del('l10n_update_import_mode');
- variable_del('l10n_update_rebuild_projects');
-}
+ variable_del('l10n_update_check_frequency');
+ variable_del('l10n_update_last_check');
+ variable_del('l10n_update_download_store');
+ variable_del('l10n_update_translation_status');
+ variable_del('l10n_update_check_mode');}
/**
* Implements hook_requirements().
*/
function l10n_update_requirements($phase) {
+ $requirements = array();
if ($phase == 'runtime') {
- if (l10n_update_get_projects() && l10n_update_language_list()) {
- $requirements['l10n_update']['title'] = t('Translation update status');
- if (l10n_update_available_updates()) {
- $requirements['l10n_update']['severity'] = REQUIREMENT_WARNING;
- $requirements['l10n_update']['value'] = t('There are available updates');
- $requirements['l10n_update']['description'] = t(
- 'There are new or updated translations available for currently installed modules and themes. To check for updates, you can visit the translation update page.',
- array(
- '@check_manually' => url('admin/config/regional/translate/update')
- )
- );
+ $available_updates = array();
+ $untranslated = array();
+ $languages = l10n_update_translatable_language_list();
+
+ if ($languages) {
+ // Determine the status of the translation updates per language.
+ $status = l10n_update_get_status();
+ if ($status) {
+ foreach ($status as $project) {
+ foreach ($project as $langcode => $project_info) {
+ if (empty($project_info->type)) {
+ $untranslated[$langcode] = $languages[$langcode];
+ }
+ elseif ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE) {
+ $available_updates[$langcode] = $languages[$langcode];
+ }
+ }
+ }
+
+ if ($available_updates || $untranslated) {
+ if ($available_updates) {
+ $requirements['l10n_update'] = array(
+ 'title' => 'Translation update status',
+ 'value' => l(t('Updates available'), 'admin/config/regional/translate/update'),
+ 'severity' => REQUIREMENT_WARNING,
+ 'description' => t('Updates available for: @languages. See the Available translation updates page for more information.', array('@languages' => implode(', ', $available_updates), '!updates' => url('admin/config/regional/translate/update'))),
+ );
+ }
+ else {
+ $requirements['l10n_update'] = array(
+ 'title' => 'Translation update status',
+ 'value' => t('Missing translations'),
+ 'severity' => REQUIREMENT_INFO,
+ 'description' => t('Missing translations for: @languages. See the Available translation updates page for more information.', array('@languages' => implode(', ', $untranslated), '!updates' => url('admin/config/regional/translate/update'))),
+ );
+ }
+ }
+ else {
+ $requirements['l10n_update'] = array(
+ 'title' => 'Translation update status',
+ 'value' => t('Up to date'),
+ 'severity' => REQUIREMENT_OK,
+ );
+ }
}
else {
- $requirements['l10n_update']['severity'] = REQUIREMENT_OK;
- $requirements['l10n_update']['value'] = t('All your translations are up to date');
+ $requirements['locale_translation'] = array(
+ 'title' => 'Translation update status',
+ 'value' => l(t('Can not determine status'), 'admin/config/regional/translate/update'),
+ 'severity' => REQUIREMENT_WARNING,
+ 'description' => t('No translation status is available. See the Available translation updates page for more information.', array('!updates' => url('admin/config/regional/translate/update'))),
+ );
}
}
- else {
- $requirements['l10n_update']['title'] = t('Translation update status');
- $requirements['l10n_update']['value'] = t('No update data available');
- $requirements['l10n_update']['severity'] = REQUIREMENT_WARNING;
- //$requirements['update_core']['reason'] = UPDATE_UNKNOWN;
- $requirements['l10n_update']['description'] = _l10n_update_no_data();
+ }
+ if ($phase == 'update') {
+ // Make sure the 'translations' stream wrapper class gets registered.
+ // This is needed when upgrading to 7.x-2.x.
+ if (!class_exists('TranslationsStreamWrapper')) {
+ registry_rebuild();
}
- return $requirements;
}
- // We must always return array, the installer doesn't use module_invoke_all()
- return array();
-}
-
-/**
- * Add status field to locales_target.
- */
-function l10n_update_update_6001() {
- if (!db_field_exists('locales_target', 'l10n_status')) {
- db_add_field('locales_target', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
- }
- return t('Added l10n_status field to locales_target.');
-}
-
-/**
- * Change status field name to l10n_status.
- */
-function l10n_update_update_6002() {
- // I18n Strings module adds a 'status' column to 'locales_target' table.
- // L10n Update module previously added a column with the same name. To avoid
- // any collision we change the column name here, but only if it was added by
- // L10n Update module.
- if (!db_field_exists('locales_target', 'l10n_status') && db_field_exists('locales_target', 'status') && !db_table_exists('i18n_strings')) {
- db_change_field('locales_target', 'status', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
- }
- // Just in case someone did install I18n Strings, we still need to make sure
- // the 'l10n_status' column gets created.
- elseif (!db_field_exists('locales_target', 'l10n_status')) {
- db_add_field('locales_target', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
- }
- return t('Resolved possible l10n_status field conflict in locales_target.');
+ return $requirements;
}
/**
@@ -282,3 +307,59 @@ function l10n_update_update_7004() {
db_create_table('cache_l10n_update', $schema);
}
}
+
+/**
+ * Migration to 7.x-2.x branch.
+ */
+function l10n_update_update_7200() {
+ // Make sure the 'translations' stream wrapper class gets registered.
+ if (!class_exists('TranslationsStreamWrapper')) {
+ registry_rebuild();
+ }
+
+ if (!variable_get('l10n_update_download_store', '')) {
+ variable_set('l10n_update_download_store', 'sites/all/translations');
+ }
+ // Create the translation directory. We try different alternative paths as the
+ // default may not always be writable.
+ $directories = array(
+ variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
+ variable_get('file_public_path', conf_path() . '/files') . '/translations',
+ 'sites/default/files/translations',
+ );
+ foreach ($directories as $directory) {
+ if (file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+ variable_set('l10n_update_download_store', $directory);
+ return;
+ }
+ }
+ watchdog('l10n_update', 'The directory %directory does not exist or is not writable.', array('%directory' => $directories[0]), WATCHDOG_ERROR);
+ drupal_set_message(t('The directory %directory does not exist or is not writable.', array('%directory' => $directories[0])), 'error');
+
+ // Translation source 'Remote server only' is no longer supported. Use 'Remote
+ // and local' instead.
+ $mode = variable_get('l10n_update_check_mode', 3); // L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL
+ if ($mode == 1) {
+ variable_set('l10n_update_check_mode', 3); // L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL
+ }
+
+ // Daily cron updates are no longer supported. Use weekly instead.
+ $frequency = variable_get('l10n_update_check_frequency', '0');
+ if ($frequency == '1') {
+ variable_set('l10n_update_check_frequency', '7');
+ }
+
+ // Clean up deprecated variables.
+ variable_del('l10n_update_default_server');
+ variable_del('l10n_update_default_server_url');
+ variable_del('l10n_update_rebuild_projects');
+}
+
+/**
+ * Sets the default translation files directory.
+ */
+function l10n_update_update_7201() {
+ if (!variable_get('l10n_update_download_store', '')) {
+ variable_set('l10n_update_download_store', 'sites/all/translations');
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.locale.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.locale.inc
deleted file mode 100644
index 2a964d2a..00000000
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.locale.inc
+++ /dev/null
@@ -1,463 +0,0 @@
- $langcode))->fetchField()) {
- drupal_set_message(t('The language selected for import is not supported.'), 'error');
- return FALSE;
- }
-
- // Get strings from file (returns on failure after a partial import, or on success)
- $status = _l10n_update_locale_import_read_po('db-store', $file, $mode, $langcode, $group);
- if ($status === FALSE) {
- // Error messages are set in _locale_import_read_po().
- return FALSE;
- }
-
- // Get status information on import process.
- list($header_done, $additions, $updates, $deletes, $skips) = _l10n_update_locale_import_one_string('db-report');
-
- if (!$header_done) {
- drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
- }
-
- // Clear cache and force refresh of JavaScript translations.
- _locale_invalidate_js($langcode);
- cache_clear_all('locale:', 'cache', TRUE);
-
- // Rebuild the menu, strings may have changed.
- menu_rebuild();
-
- watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
- if ($skips) {
- watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
- }
-
- // Return results of this import.
- return array(
- 'file' => $file,
- 'language' => $langcode,
- 'add' => $additions,
- 'update' => $updates,
- 'delete' => $deletes,
- 'skip' => $skips,
- );
-}
-
-/**
- * Parses Gettext Portable Object file into an array
- *
- * @param $op
- * Storage operation type: db-store or mem-store
- * @param $file
- * Drupal file object corresponding to the PO file to import
- * @param $mode
- * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
- * @param $lang
- * Language code
- * @param $group
- * Text group to import PO file into (eg. 'default' for interface translations)
- */
-function _l10n_update_locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
-
- $fd = fopen($file->uri, "rb"); // File will get closed by PHP on return
- if (!$fd) {
- _locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
- return FALSE;
- }
-
- $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR
- $current = array(); // Current entry being read
- $plural = 0; // Current plural form
- $lineno = 0; // Current line
-
- while (!feof($fd)) {
- $line = fgets($fd, 10*1024); // A line should not be this long
- if ($lineno == 0) {
- // The first line might come with a UTF-8 BOM, which should be removed.
- $line = str_replace("\xEF\xBB\xBF", '', $line);
- }
- $lineno++;
- $line = trim(strtr($line, array("\\\n" => "")));
-
- if (!strncmp("#", $line, 1)) { // A comment
- if ($context == "COMMENT") { // Already in comment context: add
- $current["#"][] = substr($line, 1);
- }
- elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
- _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
- $current = array();
- $current["#"][] = substr($line, 1);
- $context = "COMMENT";
- }
- else { // Parse error
- _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
- return FALSE;
- }
- }
- elseif (!strncmp("msgid_plural", $line, 12)) {
- if ($context != "MSGID") { // Must be plural form for current entry
- _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
- return FALSE;
- }
- $line = trim(substr($line, 12));
- $quoted = _locale_import_parse_quoted($line);
- if ($quoted === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- $current["msgid"] = $current["msgid"] . "\0" . $quoted;
- $context = "MSGID_PLURAL";
- }
- elseif (!strncmp("msgid", $line, 5)) {
- if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
- _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
- $current = array();
- }
- elseif ($context == "MSGID") { // Already in this context? Parse error
- _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
- return FALSE;
- }
- $line = trim(substr($line, 5));
- $quoted = _locale_import_parse_quoted($line);
- if ($quoted === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- $current["msgid"] = $quoted;
- $context = "MSGID";
- }
- elseif (!strncmp("msgctxt", $line, 7)) {
- if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
- _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
- $current = array();
- }
- elseif (!empty($current["msgctxt"])) { // Already in this context? Parse error
- _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
- return FALSE;
- }
- $line = trim(substr($line, 7));
- $quoted = _locale_import_parse_quoted($line);
- if ($quoted === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- $current["msgctxt"] = $quoted;
- $context = "MSGCTXT";
- }
- elseif (!strncmp("msgstr[", $line, 7)) {
- if (($context != "MSGID") && ($context != "MSGCTXT") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgxtxt, msgid_plural, or msgstr[]
- _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
- return FALSE;
- }
- if (strpos($line, "]") === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- $frombracket = strstr($line, "[");
- $plural = substr($frombracket, 1, strpos($frombracket, "]") - 1);
- $line = trim(strstr($line, " "));
- $quoted = _locale_import_parse_quoted($line);
- if ($quoted === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- $current["msgstr"][$plural] = $quoted;
- $context = "MSGSTR_ARR";
- }
- elseif (!strncmp("msgstr", $line, 6)) {
- if (($context != "MSGID") && ($context != "MSGCTXT")) { // Should come just after a msgid or msgctxt block
- _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
- return FALSE;
- }
- $line = trim(substr($line, 6));
- $quoted = _locale_import_parse_quoted($line);
- if ($quoted === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- $current["msgstr"] = $quoted;
- $context = "MSGSTR";
- }
- elseif ($line != "") {
- $quoted = _locale_import_parse_quoted($line);
- if ($quoted === FALSE) {
- _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
- return FALSE;
- }
- if (($context == "MSGID") || ($context == "MSGID_PLURAL")) {
- $current["msgid"] .= $quoted;
- }
- elseif ($context == "MSGCTXT") {
- $current["msgctxt"] .= $quoted;
- }
- elseif ($context == "MSGSTR") {
- $current["msgstr"] .= $quoted;
- }
- elseif ($context == "MSGSTR_ARR") {
- $current["msgstr"][$plural] .= $quoted;
- }
- else {
- _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
- return FALSE;
- }
- }
- }
-
- // End of PO file, flush last entry.
- if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) {
- _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
- }
- elseif ($context != "COMMENT") {
- _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
- return FALSE;
- }
-
-}
-
-/**
- * Imports a string into the database
- *
- * @param $op
- * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'
- * @param $value
- * Details of the string stored
- * @param $mode
- * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
- * @param $lang
- * Language to store the string in
- * @param $file
- * Object representation of file being imported, only required when op is 'db-store'
- * @param $group
- * Text group to import PO file into (eg. 'default' for interface translations)
- */
-function _l10n_update_locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
- $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
- $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
- $strings = &drupal_static(__FUNCTION__ . ':strings', array());
-
- switch ($op) {
- // Return stored strings
- case 'mem-report':
- return $strings;
-
- // Store string in memory (only supports single strings)
- case 'mem-store':
- $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
- return;
-
- // Called at end of import to inform the user
- case 'db-report':
- return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
-
- // Store the string we got in the database.
- case 'db-store':
- // We got header information.
- if ($value['msgid'] == '') {
- $languages = language_list();
- if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
- // Since we only need to parse the header if we ought to update the
- // plural formula, only run this if we don't need to keep existing
- // data untouched or if we don't have an existing plural formula.
- $header = _locale_import_parse_header($value['msgstr']);
-
- // Get the plural formula and update in database.
- if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
- list($nplurals, $plural) = $p;
- db_update('languages')
- ->fields(array(
- 'plurals' => $nplurals,
- 'formula' => $plural,
- ))
- ->condition('language', $lang)
- ->execute();
- }
- else {
- db_update('languages')
- ->fields(array(
- 'plurals' => 0,
- 'formula' => '',
- ))
- ->condition('language', $lang)
- ->execute();
- }
- }
- $header_done = TRUE;
- }
-
- else {
- // Some real string to import.
- $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
-
- if (strpos($value['msgid'], "\0")) {
- // This string has plural versions.
- $english = explode("\0", $value['msgid'], 2);
- $entries = array_keys($value['msgstr']);
- for ($i = 3; $i <= count($entries); $i++) {
- $english[] = $english[1];
- }
- $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
- $english = array_map('_locale_import_append_plural', $english, $entries);
- foreach ($translation as $key => $trans) {
- if ($key == 0) {
- $plid = 0;
- }
- $plid = _l10n_update_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, L10N_UPDATE_STRING_DEFAULT, $plid, $key);
- }
- }
-
- else {
- // A simple string to import.
- $english = $value['msgid'];
- $translation = $value['msgstr'];
- _l10n_update_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode);
- }
- }
- } // end of db-store operation
-}
-
-/**
- * Import one string into the database.
- *
- * @param $report
- * Report array summarizing the number of changes done in the form:
- * array(inserts, updates, deletes).
- * @param $langcode
- * Language code to import string into.
- * @param $context
- * The context of this string.
- * @param $source
- * Source string.
- * @param $translation
- * Translation to language specified in $langcode.
- * @param $textgroup
- * Name of textgroup to store translation in.
- * @param $location
- * Location value to save with source string.
- * @param $mode
- * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
- * @param $status
- * Status of translation if created: L10N_UPDATE_STRING_DEFAULT or L10N_UPDATE_STRING_CUSTOM
- * @param $plid
- * Optional plural ID to use.
- * @param $plural
- * Optional plural value to use.
- * @return
- * The string ID of the existing string modified or the new string added.
- */
-function _l10n_update_locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $status = L10N_UPDATE_STRING_DEFAULT, $plid = 0, $plural = 0) {
- $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField();
-
- if (!empty($translation)) {
- // Skip this string unless it passes a check for dangerous code.
- // Text groups other than default still can contain HTML tags
- // (i.e. translatable blocks).
- if ($textgroup == "default" && !locale_string_is_safe($translation)) {
- $report['skips']++;
- $lid = 0;
- watchdog('locale', 'Disallowed HTML detected. String not imported: %string', array('%string' => $translation), WATCHDOG_WARNING);
- }
- elseif ($lid) {
- // We have this source string saved already.
- db_update('locales_source')
- ->fields(array(
- 'location' => $location,
- ))
- ->condition('lid', $lid)
- ->execute();
-
- $exists = db_query("SELECT lid, l10n_status FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchObject();
-
- if (!$exists) {
- // No translation in this language.
- db_insert('locales_target')
- ->fields(array(
- 'lid' => $lid,
- 'language' => $langcode,
- 'translation' => $translation,
- 'plid' => $plid,
- 'plural' => $plural,
- ))
- ->execute();
-
- $report['additions']++;
- }
- elseif (($exists->l10n_status == L10N_UPDATE_STRING_DEFAULT && $mode == LOCALE_UPDATE_OVERRIDE_DEFAULT) || $mode == LOCALE_IMPORT_OVERWRITE) {
- // Translation exists, only overwrite if instructed.
- db_update('locales_target')
- ->fields(array(
- 'translation' => $translation,
- 'plid' => $plid,
- 'plural' => $plural,
- ))
- ->condition('language', $langcode)
- ->condition('lid', $lid)
- ->execute();
-
- $report['updates']++;
- }
- }
- else {
- // No such source string in the database yet.
- $lid = db_insert('locales_source')
- ->fields(array(
- 'location' => $location,
- 'source' => $source,
- 'context' => (string) $context,
- 'textgroup' => $textgroup,
- ))
- ->execute();
-
- db_insert('locales_target')
- ->fields(array(
- 'lid' => $lid,
- 'language' => $langcode,
- 'translation' => $translation,
- 'plid' => $plid,
- 'plural' => $plural,
- 'l10n_status' => $status,
- ))
- ->execute();
-
- $report['additions']++;
- }
- }
- elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
- // Empty translation, remove existing if instructed.
- db_delete('locales_target')
- ->condition('language', $langcode)
- ->condition('lid', $lid)
- ->condition('plid', $plid)
- ->condition('plural', $plural)
- ->execute();
-
- $report['deletes']++;
- }
-
- return $lid;
-}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.module b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.module
index 0698a68f..09e93bb8 100644
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.module
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.module
@@ -3,41 +3,102 @@
/**
* @file
* Download translations from remote localization server.
+ */
+
+/**
+ * Translation update mode: Use local files only.
*
- * @todo Fetch information from info files.
+ * When checking for available translation updates, only local files will be
+ * used. Any remote translation file will be ignored. Also custom modules and
+ * themes which have set a "server pattern" to use a remote translation server
+ * will be ignored.
*/
+define('L10N_UPDATE_USE_SOURCE_LOCAL', 2);
/**
- * Update mode: Remote server.
+ * Translation update mode: Use both remote and local files.
+ *
+ * When checking for available translation updates, both local and remote files
+ * will be checked.
*/
-define('L10N_UPDATE_CHECK_REMOTE', 1);
+define('L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL', 3);
/**
- * Update mode: Local server.
+ * Default location of gettext file on the translation server.
+ *
+ * @see l10n_update_default_translation_server().
*/
-define('L10N_UPDATE_CHECK_LOCAL', 2);
+define('L10N_UPDATE_DEFAULT_SERVER_PATTERN', 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po');
/**
- * Update mode: both.
+ * Default gettext file name on the translation server.
*/
-define('L10N_UPDATE_CHECK_ALL', L10N_UPDATE_CHECK_REMOTE | L10N_UPDATE_CHECK_LOCAL);
+define('L10N_UPDATE_DEFAULT_FILE_NAME', '%project-%release.%language.po');
/**
- * Translation import mode keeping translations which are edited after enabling
- * Locale Update module an only override default (un-edited) translations.
+ * Default gettext file name on the translation server.
*/
-define('LOCALE_UPDATE_OVERRIDE_DEFAULT', 2);
+define('L10N_UPDATE_DEFAULT_TRANSLATION_PATH', 'sites/all/translations');
/**
- * The maximum number of projects which are checked for available translations each cron run.
+ * The number of seconds that the translations status entry should be considered.
*/
-define('L10N_UPDATE_CRON_PROJECTS', 10);
+define('L10N_UPDATE_STATUS_TTL', 600);
/**
- * The maximum number of projects which are updated each cron run.
+ * UI option for override of existing translations. Only override non-customized
+ * translations.
*/
-define('L10N_UPDATE_CRON_UPDATES', 2);
+define('L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED', 2);
+/**
+ * Translation source is a remote file.
+ */
+define('L10N_UPDATE_REMOTE', 'remote');
+
+/**
+ * Translation source is a local file.
+ */
+define('L10N_UPDATE_LOCAL', 'local');
+
+/**
+ * Translation source is the current translation.
+ */
+define('L10N_UPDATE_CURRENT', 'current');
+
+/**
+ * The delimiter used to split plural strings.
+ *
+ * This is the ETX (End of text) character and is used as a minimal means to
+ * separate singular and plural variants in source and translation text. It
+ * was found to be the most compatible delimiter for the supported databases.
+ */
+define('L10N_UPDATE_PLURAL_DELIMITER', "\03");
+
+/**
+ * Flag for locally not customized interface translation.
+ *
+ * Such translations are imported from .po files downloaded from
+ * localize.drupal.org for example.
+ */
+define('L10N_UPDATE_NOT_CUSTOMIZED', 0);
+
+/**
+ * Flag for locally customized interface translation.
+ *
+ * Strings are customized when translated or edited using the build in
+ * string translation form. Strings can also be marked as customized when a po
+ * file is imported.
+ */
+define('L10N_UPDATE_STRING_CUSTOM', 1);
+
+/**
+ * Flag for locally customized interface translation.
+ *
+ * Such translations are edited from their imported originals on the user
+ * interface or are imported as customized.
+ */
+define('L10N_UPDATE_CUSTOMIZED', 1);
/**
* Implements hook_help().
@@ -45,8 +106,8 @@ define('L10N_UPDATE_CRON_UPDATES', 2);
function l10n_update_help($path, $arg) {
switch ($path) {
case 'admin/config/regional/translate/update':
- $output = '' . t('List of latest imported translations and available updates for each enabled project and language.') . '
';
- $output .= '' . t('If there are available updates you can click on Update for them to be downloaded and imported now or you can edit the configuration for them to be updated automatically on the Update settings page', array('@update-settings' => url('admin/config/regional/language/update'))) . '
';
+ $output = '' . t('Status of interface translations for each of the enabled languages.') . '
';
+ $output .= '' . t('If there are available updates you can click on "Update translation" for them to be downloaded and imported now or you can edit the configuration for them to be updated automatically on the Update settings page', array('@update-settings' => url('admin/config/regional/language/update'))) . '
';
return $output;
break;
case 'admin/config/regional/language/update':
@@ -63,12 +124,22 @@ function l10n_update_menu() {
$items['admin/config/regional/translate/update'] = array(
'title' => 'Update',
'description' => 'Available updates',
- 'page callback' => 'l10n_update_admin_overview',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('l10n_update_status_form'),
'access arguments' => array('translate interface'),
'file' => 'l10n_update.admin.inc',
'weight' => 20,
'type' => MENU_LOCAL_TASK,
);
+ $items['admin/config/regional/translate/check'] = array(
+ 'title' => 'Update',
+ 'description' => 'Available updates',
+ 'page callback' => 'l10n_update_manual_status',
+ 'access arguments' => array('translate interface'),
+ 'file' => 'l10n_update.admin.inc',
+ 'weight' => 20,
+ 'type' => MENU_CALLBACK,
+ );
$items['admin/config/regional/language/update'] = array(
'title' => 'Translation updates',
'description' => 'Automatic update configuration',
@@ -82,12 +153,31 @@ function l10n_update_menu() {
return $items;
}
+/**
+ * Implements hook_theme().
+ */
+function l10n_update_theme() {
+ return array(
+ 'l10n_update_last_check' => array(
+ 'variables' => array('last' => NULL),
+ 'file' => 'l10n_update.admin.inc',
+ 'template' => 'l10n_update-translation-last-check',
+ ),
+ 'l10n_update_update_info' => array(
+ 'variables' => array('updates' => array(), 'not_found' => array()),
+ 'file' => 'l10n_update.admin.inc',
+ 'template' => 'l10n_update-translation-update-info',
+ ),
+ );
+}
/**
* Implements hook_menu_alter().
*/
function l10n_update_menu_alter(&$menu) {
// Redirect l10n_client AJAX callback path for strings.
- $menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
+ if (module_exists('l10n_client')) {
+ $menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
+ }
}
/**
@@ -96,21 +186,110 @@ function l10n_update_menu_alter(&$menu) {
* Check one project/language at a time, download and import if update available
*/
function l10n_update_cron() {
- if ($frequency = variable_get('l10n_update_check_frequency', 0)) {
- module_load_include('check.inc', 'l10n_update');
- list($checked, $updated) = l10n_update_check_translations(L10N_UPDATE_CRON_PROJECTS, REQUEST_TIME - $frequency * 24 * 3600, L10N_UPDATE_CRON_UPDATES);
- watchdog('l10n_update', 'Automatically checked @checked translations, updated @updated.', array('@checked' => count($checked), '@updated' => count($updated)));
+ // Update translations only when an update frequency was set by the admin
+ // and a translatable language was set.
+ // Update tasks are added to the queue here but processed by Drupal's cron
+ // using the cron worker defined in l10n_update_queue_info().
+ if ($frequency = variable_get('l10n_update_check_frequency', '0') && l10n_update_translatable_language_list()) {
+ module_load_include('translation.inc', 'l10n_update');
+ l10n_update_cron_fill_queue();
}
}
+/**
+ * Implements hook_cron_queue_info().
+ */
+function l10n_update_cron_queue_info() {
+ $queues['l10n_update'] = array(
+ 'worker callback' => 'l10n_update_worker',
+ 'time' => 30,
+ );
+ return $queues;
+}
+
+/**
+ * Callback: Executes interface translation queue tasks.
+ *
+ * The translation update functions executed here are batch operations which
+ * are also used in translation update batches. The batch functions may need to
+ * be executed multiple times to complete their task, typically this is the
+ * translation import function. When a batch function is not finished, a new
+ * queue task is created and added to the end of the queue. The batch context
+ * data is needed to continue the batch task is stored in the queue with the
+ * queue data.
+ *
+ * @param array $data
+ * Queue data array containing:
+ * - Function name.
+ * - Array of function arguments. Optionally contains the batch context data.
+ *
+ * @see l10n_update_queue_info()
+ */
+function l10n_update_worker($data) {
+ module_load_include('batch.inc', 'l10n_update');
+ list($function, $args) = $data;
+
+ // We execute batch operation functions here to check, download and import the
+ // translation files. Batch functions use a context variable as last argument
+ // which is passed by reference. When a batch operation is called for the
+ // first time a default batch context is created. When called iterative
+ // (usually the batch import function) the batch context is passed through via
+ // the queue and is part of the $data.
+ $last = count($args) - 1;
+ if (!is_array($args[$last]) || !isset($args[$last]['finished'])) {
+ $batch_context = array(
+ 'sandbox' => array(),
+ 'results' => array(),
+ 'finished' => 1,
+ 'message' => '',
+ );
+ }
+ else {
+ $batch_context = $args[$last];
+ unset ($args[$last]);
+ }
+ $args = array_merge($args, array(&$batch_context));
+
+ // Call the batch operation function.
+ call_user_func_array($function, $args);
+
+ // If the batch operation is not finished we create a new queue task to
+ // continue the task. This is typically the translation import task.
+ if ($batch_context['finished'] < 1) {
+ unset($batch_context['strings']);
+ $queue = DrupalQueue::get('l10n_update', TRUE);
+ $queue->createItem(array($function, $args));
+ }
+}
+
+/**
+ * Implements hook_stream_wrappers().
+ */
+function l10n_update_stream_wrappers() {
+ // Load the stream wrapper class if not automatically loaded. This happens
+ // before update.php is executed.
+ if (!class_exists('TranslationsStreamWrapper')) {
+ require_once('includes/locale/TranslationsStreamWrapper.php');
+ }
+
+ $wrappers['translations'] = array(
+ 'name' => t('Translation files'),
+ 'class' => 'TranslationsStreamWrapper',
+ 'description' => t('Translation files.'),
+ 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
+ );
+
+ return $wrappers;
+}
+
/**
* Implements hook_form_alter().
*/
function l10n_update_form_alter(&$form, $form_state, $form_id) {
switch ($form_id) {
case 'locale_translate_edit_form':
- // Replace the submit callback by our own customized version
- $form['#submit'] = array('l10n_update_locale_translate_edit_form_submit');
+ case 'i18n_string_locale_translate_edit_form':
+ $form['#submit'][] = 'l10n_update_locale_translate_edit_form_submit';
break;
case 'locale_languages_predefined_form':
case 'locale_languages_custom_form':
@@ -129,8 +308,24 @@ function l10n_update_form_alter(&$form, $form_state, $form_id) {
* Refresh project translation status and get translations if required.
*/
function l10n_update_modules_enabled($modules) {
- module_load_include('project.inc', 'l10n_update');
- l10n_update_project_refresh($modules);
+ $components['module'] = $modules;
+ l10n_update_system_update($components);
+}
+
+/**
+ * Implements hook_modules_disabled().
+ *
+ * Set disabled modules to be ignored when updating translations.
+ */
+function l10n_update_modules_disabled($modules) {
+ if (!variable_get('l10n_update_check_disabled', FALSE)) {
+ db_update('l10n_update_project')
+ ->fields(array(
+ 'status' => 0,
+ ))
+ ->condition('name', $modules)
+ ->execute();
+ }
}
/**
@@ -140,29 +335,40 @@ function l10n_update_modules_enabled($modules) {
* rebuild the projects cache.
*/
function l10n_update_modules_uninstalled($modules) {
- db_delete('l10n_update_file')
- ->condition('project', $modules)
- ->execute();
-
- // Rebuild {l10n_update_project} table.
- // Just like the system table, the project table holds both enabled and
- // disabled projects. Full control over its content is not possible.
- // To minimize polution we flush it here. The cost of rebuilding is small
- // compared to the {l10n_update_file} table.
- db_delete('l10n_update_project')->execute();
- module_load_include('project.inc', 'l10n_update');
- l10n_update_build_projects();
+ $components['module'] = $modules;
+ l10n_update_system_remove($components);
}
/**
- * Aditional submit handler for language forms
+ * Implements hook_themes_enabled().
+ *
+ * Refresh project translation status and get translations if required.
+ */
+function l10n_update_themes_enabled($themes) {
+ $components['theme'] = $themes;
+ l10n_update_system_update($components);
+}
+
+/**
+ * Additional submit handler for language forms
*
* We need to refresh status when a new language is enabled / disabled
*/
function l10n_update_languages_changed_submit($form, $form_state) {
- module_load_include('check.inc', 'l10n_update');
- $langcode = $form_state['values']['langcode'];
- l10n_update_language_refresh(array($langcode));
+ if (variable_get('l10n_update_import_enabled', TRUE)) {
+ if (empty($form_state['values']['predefined_langcode']) || $form_state['values']['predefined_langcode'] == 'custom') {
+ $langcode = $form_state['values']['langcode'];
+ }
+ else {
+ $langcode = $form_state['values']['predefined_langcode'];
+ }
+
+ // Download and import translations for the newly added language.
+ module_load_include('fetch.inc', 'l10n_update');
+ $options = _l10n_update_default_update_options();
+ $batch = l10n_update_batch_update_build(array(), array($langcode), $options);
+ batch_set($batch);
+ }
}
/**
@@ -172,67 +378,28 @@ function l10n_update_languages_changed_submit($form, $form_state) {
*/
function l10n_update_languages_delete_submit($form, $form_state) {
$langcode = $form_state['values']['langcode'];
- module_load_include('inc', 'l10n_update');
- l10n_update_delete_file_history($langcode);
+ l10n_update_file_history_delete(array(), $langcode);
}
/**
- * Replacement submit handler for translation edit form.
+ * Additional submit handler for locale and i18n_string translation edit form.
*
- * Process string editing form submissions marking translations as customized.
- * Saves all translations of one string submitted from a form.
+ * Mark locally edited translations as customized.
*
* @see l10n_update_form_alter()
- * @todo Just mark as customized when string changed.
*/
function l10n_update_locale_translate_edit_form_submit($form, &$form_state) {
- module_load_include('inc', 'l10n_update');
$lid = $form_state['values']['lid'];
- foreach ($form_state['values']['translations'] as $key => $value) {
- $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField();
- if (!empty($value)) {
- // Only update or insert if we have a value to use.
- if (!empty($translation)) {
- db_update('locales_target')
- ->fields(array(
- 'translation' => $value,
- 'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
- ))
- ->condition('lid', $lid)
- ->condition('language', $key)
- ->execute();
- }
- else {
- db_insert('locales_target')
- ->fields(array(
- 'lid' => $lid,
- 'translation' => $value,
- 'language' => $key,
- 'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
- ))
- ->execute();
- }
- }
- elseif (!empty($translation)) {
- // Empty translation entered: remove existing entry from database.
- db_delete('locales_target')
+ foreach ($form_state['values']['translations'] as $langcode => $value) {
+ if (!empty($value) && $value != $form_state['complete form']['translations'][$langcode]['#default_value']) {
+ // An update has been made, mark the string as customized.
+ db_update('locales_target')
+ ->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
->condition('lid', $lid)
- ->condition('language', $key)
+ ->condition('language', $langcode)
->execute();
}
-
- // Force JavaScript translation file recreation for this language.
- _locale_invalidate_js($key);
}
-
- drupal_set_message(t('The string has been saved.'));
-
- // Clear locale cache.
- _locale_invalidate_js();
- cache_clear_all('locale:', 'cache', TRUE);
-
- $form_state['redirect'] = 'admin/config/regional/translate/translate';
- return;
}
/**
@@ -246,9 +413,9 @@ function l10n_update_client_save_string() {
// Ensure we have this source string before we attempt to save it.
// @todo: add actual context support.
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $_POST['source'], ':context' => '', ':textgroup' => $_POST['textgroup']))->fetchField();
-
+
if (!empty($lid)) {
- module_load_include('inc', 'l10n_update');
+ module_load_include('translation.inc', 'l10n_update');
$report = array('skips' => 0, 'additions' => 0, 'updates' => 0, 'deletes' => 0);
// @todo: add actual context support.
_l10n_update_locale_import_one_string_db($report, $language->language, '', $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE, L10N_UPDATE_STRING_CUSTOM);
@@ -296,232 +463,299 @@ function l10n_update_client_save_string() {
}
/**
- * Get stored list of projects
+ * Imports translations when new modules or themes are installed.
*
- * @param boolean $refresh
- * TRUE = refresh the project data.
- * @param boolean $disabled
- * TRUE = get enabled AND disabled projects.
- * FALSE = get enabled projects only.
+ * This function will start a batch to import translations for the added
+ * components.
*
- * @return array
- * Array of project objects keyed by project name.
+ * @param array $components
+ * An array of arrays of component (theme and/or module) names to import
+ * translations for, indexed by type.
*/
-function l10n_update_get_projects($refresh = FALSE, $disabled = FALSE) {
- static $projects, $enabled;
+function l10n_update_system_update(array $components) {
+ $components += array('module' => array(), 'theme' => array());
+ $list = array_merge($components['module'], $components['theme']);
- if (!isset($projects) || $refresh) {
- if (variable_get('l10n_update_rebuild_projects', 0)) {
- module_load_include('project.inc', 'l10n_update');
- variable_del('l10n_update_rebuild_projects');
- l10n_update_build_projects();
- }
- $projects = $enabled = array();
- $result = db_query('SELECT * FROM {l10n_update_project}');
- foreach ($result as $project) {
- $projects[$project->name] = $project;
- if ($project->status) {
- $enabled[$project->name] = $project;
- }
+ // Skip running the translation imports if in the installer,
+ // because it would break out of the installer flow. We have
+ // built-in support for translation imports in the installer.
+ if (!drupal_installation_attempted() && l10n_update_translatable_language_list() && variable_get('l10n_update_import_enabled', TRUE)) {
+ module_load_include('compare.inc', 'l10n_update');
+
+ // Update the list of translatable projects and start the import batch.
+ // Only when new projects are added the update batch will be triggered. Not
+ // each enabled module will introduce a new project. E.g. sub modules.
+ $projects = array_keys(l10n_update_build_projects());
+ if ($list = array_intersect($list, $projects)) {
+ module_load_include('fetch.inc', 'l10n_update');
+ // Get translation status of the projects, download and update translations.
+ $options = _l10n_update_default_update_options();
+ $batch = l10n_update_batch_update_build($list, array(), $options);
+ batch_set($batch);
}
}
- return $disabled ? $projects : $enabled;
}
/**
- * Get server information, that can come from different sources.
+ * Delete translation history of modules and themes.
*
- * - From server list provided by modules. They can provide full server information or just the url
- * - From server_url in a project, we'll fetch latest data from the server itself
+ * Only the translation history is removed, not the source strings or
+ * translations. This is not possible because strings are shared between
+ * modules and we have no record of which string is used by which module.
*
- * @param string $name
- * Server name e.g. localize.drupal.org
- * @param string $url
- * Server url
- * @param boolean $refresh
- * TRUE = refresh the server data.
+ * @param array $components
+ * An array of arrays of component (theme and/or module) names to import
+ * translations for, indexed by type.
+ */
+function l10n_update_system_remove($components) {
+ $components += array('module' => array(), 'theme' => array());
+ $list = array_merge($components['module'], $components['theme']);
+ if ($language_list = l10n_update_translatable_language_list()) {
+ module_load_include('compare.inc', 'l10n_update');
+ module_load_include('bulk.inc', 'l10n_update');
+
+ // Only when projects are removed, the translation files and records will be
+ // deleted. Not each disabled module will remove a project. E.g. sub modules.
+ $projects = array_keys(l10n_update_get_projects());
+ if ($list = array_intersect($list, $projects)) {
+ l10n_update_file_history_delete($list);
+
+ // Remove translation files.
+ l10n_update_delete_translation_files($list, array());
+
+ // Remove translatable projects.
+ // Followup issue http://drupal.org/node/1842362 to replace the
+ // {l10n_update_project} table. Then change this to a function call.
+ db_delete('l10n_update_project')
+ ->condition('name', $list)
+ ->execute();
+
+ // Clear the translation status.
+ l10n_update_status_delete_projects($list);
+ }
+
+ }
+}
+
+/**
+ * Gets current translation status from the {l10n_update_file} table.
*
* @return array
- * Array of server data.
+ * Array of translation file objects.
*/
-function l10n_update_server($name = NULL, $url = NULL, $refresh = FALSE) {
- static $info, $server_list;
+function l10n_update_get_file_history() {
+ $history = &drupal_static(__FUNCTION__, array());
- // Retrieve server list from modules
- if (!isset($server_list) || $refresh) {
- $server_list = module_invoke_all('l10n_servers');
- }
- // We need at least the server url to fetch all the information
- if (!$url && $name && isset($server_list[$name])) {
- $url = $server_list[$name]['server_url'];
- }
- // If we still don't have an url, cannot find this server, return false
- if (!$url) {
- return FALSE;
- }
- // Cache server information based on the url, refresh if asked
- $cid = 'l10n_update_server:' . $url;
- if ($refresh) {
- unset($info);
- cache_clear_all($cid, 'cache_l10n_update');
- }
- if (!isset($info[$url])) {
- if ($cache = cache_get($cid, 'cache_l10n_update')) {
- $info[$url] = $cache->data;
+ if (empty($history)) {
+ // Get file history from the database.
+ $result = db_query('SELECT project, language, filename, version, uri, timestamp, last_checked FROM {l10n_update_file}');
+ foreach ($result as $file) {
+ $file->langcode = $file->language;
+ $file->type = $file->timestamp ? L10N_UPDATE_CURRENT : '';
+ $history[$file->project][$file->langcode] = $file;
}
- else {
- module_load_include('parser.inc', 'l10n_update');
- if ($name && !empty($server_list[$name])) {
- // The name is in our list, it can be full data or just an url
- $server = $server_list[$name];
+ }
+ return $history;
+}
+
+/**
+ * Updates the {locale_file} table.
+ *
+ * @param object $file
+ * Object representing the file just imported.
+ *
+ * @return integer
+ * FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
+ *
+ * @see drupal_write_record()
+ */
+function l10n_update_update_file_history($file) {
+ // Update or write new record.
+ if (db_query("SELECT project FROM {l10n_update_file} WHERE project = :project AND language = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) {
+ $update = array('project', 'language');
+ }
+ else {
+ $update = array();
+ }
+ $file->language = $file->langcode;
+ $result = drupal_write_record('l10n_update_file', $file, $update);
+ // The file history has changed, flush the static cache now.
+ // @todo Can we make this more fine grained?
+ drupal_static_reset('l10n_update_get_file_history');
+ return $result;
+}
+
+/**
+ * Deletes the history of downloaded translations.
+ *
+ * @param array $projects
+ * Project name(s) to be deleted from the file history. If both project(s) and
+ * language code(s) are specified the conditions will be ANDed.
+ * @param array $langcode
+ * Language code(s) to be deleted from the file history.
+ */
+function l10n_update_file_history_delete($projects = array(), $langcodes = array()) {
+ $query = db_delete('l10n_update_file');
+ if (!empty($projects)) {
+ $query->condition('project', $projects);
+ }
+ if (!empty($langcodes)) {
+ $query->condition('language', $langcodes);
+ }
+ $query->execute();
+}
+
+/**
+ * Gets the current translation status.
+ *
+ * @todo What is 'translation status'?
+ */
+function l10n_update_get_status($projects = NULL, $langcodes = NULL) {
+ $result = array();
+ $status = variable_get('l10n_update_translation_status', array());
+ module_load_include('translation.inc', 'l10n_update');
+ $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+
+ // Get the translation status of each project-language combination. If no
+ // status was stored, a new translation source is created.
+ foreach ($projects as $project) {
+ foreach ($langcodes as $langcode) {
+ if (isset($status[$project][$langcode])) {
+ $result[$project][$langcode] = $status[$project][$langcode];
}
else {
- // This may be a new server provided by a module / package
- $server = array('name' => $name, 'server_url' => $url);
- // If searching by name, store the name => url mapping
- if ($name) {
- $server_list[$name] = $server;
+ $sources = l10n_update_build_sources(array($project), array($langcode));
+ if (isset($sources[$project][$langcode])) {
+ $result[$project][$langcode] = $sources[$project][$langcode];
}
}
- // Now fetch server meta information form the server itself
- if ($server = l10n_update_get_server($server)) {
- cache_set($cid, $server, 'cache_l10n_update');
- $info[$url] = $server;
- }
- else {
- // If no server information, this will be FALSE. We won't search a server twice
- $info[$url] = FALSE;
- }
}
}
- return $info[$url];
+ return $result;
}
/**
- * Implements hook_l10n_servers().
+ * Saves the status of translation sources in static cache.
*
- * @return array
- * Array of server data:
- * 'name' => server name
- * 'server_url' => server url
- * 'update_url' => update url
+ * @param string $project
+ * Machine readable project name.
+ * @param string $langcode
+ * Language code.
+ * @param string $type
+ * Type of data to be stored.
+ * @param array $data
+ * File object also containing timestamp when the translation is last updated.
*/
-function l10n_update_l10n_servers() {
- module_load_include('inc', 'l10n_update');
- $server = l10n_update_default_server();
- return array($server['name'] => $server);
-}
+function l10n_update_status_save($project, $langcode, $type, $data) {
+ // Followup issue: http://drupal.org/node/1842362
+ // Split status storage per module/language and expire individually. This will
+ // improve performance for large sites.
-/**
- * Get update history.
- *
- * @param boolean $refresh
- * TRUE = refresh the history data.
- * @return
- * An array of translation files indexed by project and language.
- */
-function l10n_update_get_history($refresh = NULL) {
- static $status;
-
- if ($refresh || !isset($status)) {
- // Now add downloads history to projects
- $result = db_query("SELECT * FROM {l10n_update_file}");
- foreach ($result as $update) {
- $status[$update->project][$update->language] = $update;
+ // Load the translation status or build it if not already available.
+ module_load_include('translation.inc', 'l10n_update');
+ $status = l10n_update_get_status();
+ if (empty($status)) {
+ $projects = l10n_update_get_projects(array($project));
+ if (isset($projects[$project])) {
+ $status[$project][$langcode] = l10n_update_source_build($projects[$project], $langcode);
}
}
- return $status;
+
+ // Merge the new status data with the existing status.
+ if (isset($status[$project][$langcode])) {
+ switch ($type) {
+ case L10N_UPDATE_REMOTE:
+ case L10N_UPDATE_LOCAL:
+ // Add the source data to the status array.
+ $status[$project][$langcode]->files[$type] = $data;
+ // Check if this translation is the most recent one. Set timestamp and
+ // data type of the most recent translation source.
+ if (isset($data->timestamp) && $data->timestamp) {
+ if ($data->timestamp > $status[$project][$langcode]->timestamp) {
+ $status[$project][$langcode]->timestamp = $data->timestamp;
+ $status[$project][$langcode]->last_checked = REQUEST_TIME;
+ $status[$project][$langcode]->type = $type;
+ }
+ }
+ break;
+ case L10N_UPDATE_CURRENT:
+ $data->last_checked = REQUEST_TIME;
+ $status[$project][$langcode]->timestamp = $data->timestamp;
+ $status[$project][$langcode]->last_checked = $data->last_checked;
+ $status[$project][$langcode]->type = $type;
+ l10n_update_update_file_history($data);
+ break;
+ }
+
+ variable_set('l10n_update_translation_status', $status);
+ variable_set('l10n_update_last_check', REQUEST_TIME);
+ }
}
/**
- * Get language list.
+ * Delete language entries from the status cache.
+ *
+ * @param array $langcodes
+ * Language code(s) to be deleted from the cache.
+ */
+function l10n_update_status_delete_languages($langcodes) {
+ if ($status = l10n_update_get_status()) {
+ foreach ($status as $project => $languages) {
+ foreach ($languages as $langcode => $source) {
+ if (in_array($langcode, $langcodes)) {
+ unset($status[$project][$langcode]);
+ }
+ }
+ }
+ variable_set('l10n_update_translation_status', $status);
+ }
+}
+
+/**
+ * Delete project entries from the status cache.
+ *
+ * @param array $projects
+ * Project name(s) to be deleted from the cache.
+ */
+function l10n_update_status_delete_projects($projects) {
+ $status = l10n_update_get_status();
+
+ foreach ($status as $project => $languages) {
+ if (in_array($project, $projects)) {
+ unset($status[$project]);
+ }
+ }
+ variable_set('l10n_update_translation_status', $status);
+}
+
+/**
+ * Returns list of translatable languages.
*
* @return array
- * Array of installed language names. English is the source language and
- * is therefore not included.
+ * Array of enabled languages keyed by language name. English is omitted.
*/
-function l10n_update_language_list() {
+function l10n_update_translatable_language_list() {
$languages = locale_language_list('name');
- // Skip English language
- if (isset($languages['en'])) {
- unset($languages['en']);
- }
+ unset($languages['en']);
return $languages;
}
/**
- * Implements hook_theme().
+ * Clear the translation status cache.
*/
-function l10n_update_theme() {
- return array(
- 'l10n_update_project_status' => array(
- 'variables' => array('projects' => NULL, 'languages' => NULL, 'history' => NULL, 'available' => NULL, 'updates' => NULL),
- 'file' => 'l10n_update.admin.inc',
- ),
- 'l10n_update_single_project_wrapper' => array(
- 'project' => array('project' => NULL, 'project_status' => NULL, 'languages' => NULL, 'history' => NULL, 'updates' => NULL),
- 'file' => 'l10n_update.admin.inc',
- ),
- 'l10n_update_single_project_status' => array(
- 'variables' => array('project' => NULL, 'server' => NULL, 'status' => NULL),
- 'file' => 'l10n_update.admin.inc',
- ),
- 'l10n_update_current_release' => array(
- 'variables' => array('language' => NULL, 'release' => NULL, 'status' => NULL),
- 'file' => 'l10n_update.admin.inc',
- ),
- 'l10n_update_available_release' => array(
- 'variables' => array('release' => NULL),
- 'file' => 'l10n_update.admin.inc',
- ),
- 'l10n_update_version_status' => array(
- 'variables' => array('status' => NULL, 'type' => NULL),
- 'file' => 'l10n_update.admin.inc',
- ),
- );
+function l10n_update_clear_status() {
+ variable_del('l10n_update_translation_status');
+ variable_del('l10n_update_last_check');
}
/**
- * Build the warning message for when there is no data about available updates.
+ * Checks whether remote translation sources are used.
*
- * @return sting
- * Message text with links.
+ * @return bool
+ * Returns TRUE if remote translations sources should be taken into account
+ * when checking or importing translation files, FALSE otherwise.
*/
-function _l10n_update_no_data() {
- $destination = drupal_get_destination();
- return t('No information is available about potential new and updated translations for currently installed modules and themes. To check for updates, you may need to run cron or you can check manually. Please note that checking for available updates can take a long time, so please be patient.', array(
- '@run_cron' => url('admin/reports/status/run-cron', array('query' => $destination)),
- '@check_manually' => url('admin/config/regional/translate/update', array('query' => $destination)),
- ));
-}
-
-/**
- * Get available updates.
- *
- * @param boolean $refresh
- * TRUE = refresh the history data.
- *
- * @return array
- * Array of all projects for which updates are available. For each project
- * an array of update objects, one per language.
- */
-function l10n_update_available_updates($refresh = NULL) {
- module_load_include('check.inc', 'l10n_update');
- if ($available = l10n_update_available_releases($refresh)) {
- $history = l10n_update_get_history();
- return l10n_update_build_updates($history, $available);
- }
-}
-
-/**
- * Implements hook_flush_caches().
- *
- * Called from update.php (among others) to flush the caches.
- */
-function l10n_update_flush_caches() {
- if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
- cache_clear_all('*', 'cache_l10n_update', TRUE);
- variable_set('l10n_update_rebuild_projects', 1);
- }
- return array();
+function l10n_update_use_remote_source() {
+ return variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL) == L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL;
}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.parser.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.parser.inc
deleted file mode 100644
index 3c27d4eb..00000000
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.parser.inc
+++ /dev/null
@@ -1,134 +0,0 @@
-data)) {
- $data[] = $xml->data;
- $parser = new l10n_update_xml_parser;
- return $parser->parse($xml->data);
- }
- else {
- return FALSE;
- }
-}
-
-/**
- * Parser for server metadata
- */
-class l10n_update_xml_parser {
- var $current_language;
- var $current_server;
- var $current_languages;
-
- var $servers;
-
-
- /**
- * Parse an XML data file.
- *
- * It can contain information for one or more l10n_servers
- *
- * Example data, http://ftp.drupal.org/files/translations/l10n_server.xml
- */
- function parse($data) {
- $parser = xml_parser_create();
- xml_set_object($parser, $this);
- xml_set_element_handler($parser, 'start', 'end');
- xml_set_character_data_handler($parser, "data");
- xml_parse($parser, $data);
- xml_parser_free($parser);
-
- //return $this->servers;
- return $this->current_server;
- }
-
- function start($parser, $name, $attr) {
- $this->current_tag = $name;
- switch ($name) {
- case 'L10N_SERVER':
- unset($this->current_object);
- $this->current_server = array();
- $this->current_object = &$this->current_server;
- break;
- case 'LANGUAGES':
- unset($this->current_object);
- $this->current_languages = array();
- $this->current_object = &$this->current_languages;
- //$this->current_object = &$this->current_release;
- break;
- case 'LANGUAGE':
- unset($this->current_object);
- $this->current_language = array();
- $this->current_object = &$this->current_language;
- break;
- }
- }
-
- function end($parser, $name) {
- switch ($name) {
- case 'L10N_SERVER':
- unset($this->current_object);
- $this->servers[$this->current_server['name']] = $this->current_server;
- //$this->current_server = array();
- break;
- case 'LANGUAGE':
- unset($this->current_object);
- $this->current_languages[$this->current_language['code']] = $this->current_language;
- $this->current_language = array();
- break;
- case 'LANGUAGES':
- $this->current_server['languages'] = $this->current_languages;
- break;
- default:
- $this->current_object[strtolower($this->current_tag)] = trim($this->current_object[strtolower($this->current_tag)]);
- $this->current_tag = '';
- }
- }
-
- function data($parser, $data) {
- if ($this->current_tag && !in_array($this->current_tag, array('L10N_SERVER', 'LANGUAGES', 'LANGUAGE'))) {
- $tag = strtolower($this->current_tag);
- if (isset($this->current_object[$tag])) {
- $this->current_object[$tag] .= $data;
- }
- else {
- $this->current_object[$tag] = $data;
- }
- }
- }
-}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.project.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.project.inc
deleted file mode 100644
index 8257be9b..00000000
--- a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.project.inc
+++ /dev/null
@@ -1,243 +0,0 @@
-fields(array(
- 'status' => 0,
- ))
- ->execute();
-
- $default_server = l10n_update_default_server();
-
- if (module_exists('update')) {
- $projects_info = update_get_available(TRUE);
- }
- foreach ($projects as $name => $data) {
- if (isset($projects_info[$name]['releases']) && $projects_info[$name]['project_status'] != 'not-fetched') {
- // Find out if a dev version is installed.
- if (preg_match("/^[0-9]+\.x-([0-9]+)\..*-dev$/", $data['info']['version'], $matches)) {
- // Find a suitable release to use as alternative translation.
- foreach ($projects_info[$name]['releases'] as $project_release) {
- // The first release with the same major release number which is not
- // a dev release is the one. Releases are sorted the most recent first.
- if ($project_release['version_major'] == $matches[1] &&
- (!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
- $release = $project_release;
- break;
- }
- }
- }
- elseif ($name == "drupal" || preg_match("/HEAD/", $data['info']['version'], $matches)) {
- // Pick latest available release.
- $release = array_shift($projects_info[$name]['releases']);
- }
-
- if (!empty($release['version'])) {
- $data['info']['version'] = $release['version'];
- }
-
- unset($release);
- }
-
- $data += array(
- 'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
- 'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
- // The project can have its own l10n server, we use default if not
- 'l10n_server' => isset($data['info']['l10n server']) ? $data['info']['l10n server'] : NULL,
- // A project can provide the server url to fetch metadata, or the update url (path)
- 'l10n_url' => isset($data['info']['l10n url']) ? $data['info']['l10n url'] : NULL,
- 'l10n_path' => isset($data['info']['l10n path']) ? $data['info']['l10n path'] : NULL,
- 'status' => 1,
- );
- $project = (object) $data;
- // Unless the project provides a full l10n path (update url), we try to build one
- if (!isset($project->l10n_path)) {
- $server = NULL;
- if ($project->l10n_server || $project->l10n_url) {
- $server = l10n_update_server($project->l10n_server, $project->l10n_url);
- }
- else {
- // Use the default server
- $server = l10n_update_server($default_server['name'], $default_server['server_url']);
- }
- if ($server) {
- // Build the update path for this project, with project name and release replaced
- $project->l10n_path = l10n_update_build_string($project, $server['update_url']);
- }
- }
- // Create / update project record
- $update = empty($current[$name]) ? array() : array('name');
- drupal_write_record('l10n_update_project', $project, $update);
- $projects[$name] = $project;
- }
- return $projects;
-}
-
-/**
- * Get update module's project list
- *
- * @return array
- */
-function l10n_update_project_list() {
- $projects = array();
- $disabled = variable_get('l10n_update_check_disabled', 0);
- // Unlike update module, this one has no cache
- _l10n_update_project_info_list($projects, system_rebuild_module_data(), 'module', $disabled);
- _l10n_update_project_info_list($projects, system_rebuild_theme_data(), 'theme', $disabled);
- // Allow other modules to alter projects before fetching and comparing.
- drupal_alter('l10n_update_projects', $projects);
- return $projects;
-}
-
-/**
- * Refresh projects after enabling modules
- *
- * When new projects are installed, set a batch for locale import / update
- *
- * @param $modules
- * Array of module names.
- */
-function l10n_update_project_refresh($modules) {
- module_load_include('check.inc', 'l10n_update');
- $projects = array();
-
- // Get all current projects, including the recently installed.
- $current_projects = l10n_update_build_projects();
- // Collect project data of newly installed projects.
- foreach ($modules as $name) {
- if (isset($current_projects[$name])) {
- $projects[$name] = $current_projects[$name];
- }
- }
-
- // If a translation is available and if update is required, lets go.
- if ($projects && $available = l10n_update_check_projects($projects)) {
- $history = l10n_update_get_history();
- if ($updates = l10n_update_build_updates($history, $available)) {
- module_load_include('batch.inc', 'l10n_update');
- // Filter out updates in other languages. If no languages, all of them will be updated
- $updates = _l10n_update_prepare_updates($updates);
- $batch = l10n_update_batch_multiple($updates, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
- batch_set($batch);
- }
- }
-}
-
-/**
- * Populate an array of project data.
- *
- * Based on _update_process_info_list()
- *
- * @param $projects
- * @param $list
- * @param $project_type
- * @param $disabled
- * TRUE to include disabled projects too
- */
-function _l10n_update_project_info_list(&$projects, $list, $project_type, $disabled = FALSE) {
- foreach ($list as $file) {
- if (!$disabled && empty($file->status)) {
- // Skip disabled modules or themes.
- continue;
- }
-
- // Skip if the .info file is broken.
- if (empty($file->info)) {
- continue;
- }
-
- // If the .info doesn't define the 'project', try to figure it out.
- if (!isset($file->info['project'])) {
- $file->info['project'] = l10n_update_get_project_name($file);
- }
-
- // If we still don't know the 'project', give up.
- if (empty($file->info['project'])) {
- continue;
- }
-
- // If we don't already know it, grab the change time on the .info file
- // itself. Note: we need to use the ctime, not the mtime (modification
- // time) since many (all?) tar implementations will go out of their way to
- // set the mtime on the files it creates to the timestamps recorded in the
- // tarball. We want to see the last time the file was changed on disk,
- // which is left alone by tar and correctly set to the time the .info file
- // was unpacked.
- if (!isset($file->info['_info_file_ctime'])) {
- $info_filename = dirname($file->uri) . '/' . $file->name . '.info';
- $file->info['_info_file_ctime'] = filectime($info_filename);
- }
-
- $project_name = $file->info['project'];
- if (!isset($projects[$project_name])) {
- // Only process this if we haven't done this project, since a single
- // project can have multiple modules or themes.
- $projects[$project_name] = array(
- 'name' => $project_name,
- 'info' => $file->info,
- 'datestamp' => isset($file->info['datestamp']) ? $file->info['datestamp'] : 0,
- 'includes' => array($file->name => $file->info['name']),
- 'project_type' => $project_name == 'drupal' ? 'core' : $project_type,
- );
- }
- else {
- $projects[$project_name]['includes'][$file->name] = $file->info['name'];
- $projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
- }
- }
-}
-
-/**
- * Given a $file object (as returned by system_rebuild_module_data()), figure
- * out what project it belongs to.
- *
- * Based on update_get_project_name().
- *
- * @param $file
- * @return string
- * @see system_get_files_database()
- */
-function l10n_update_get_project_name($file) {
- $project_name = '';
- if (isset($file->info['project'])) {
- $project_name = $file->info['project'];
- }
- elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
- $project_name = 'drupal';
- }
- return $project_name;
-}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/l10n_update.translation.inc b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.translation.inc
new file mode 100644
index 00000000..05f82cca
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/l10n_update.translation.inc
@@ -0,0 +1,570 @@
+rowCount() == 0) {
+ module_load_include('compare.inc', 'l10n_update');
+ // At least the core project should be in the database, so we build the
+ // data if none are found.
+ l10n_update_build_projects();
+ $result = db_query('SELECT name, project_type, core, version, l10n_path as server_pattern, status FROM {l10n_update_project}');
+ }
+
+ foreach ($result as $project) {
+ $projects[$project->name] = $project;
+ }
+ }
+
+ // Return the requested project names or all projects.
+ if ($project_names) {
+ return array_intersect_key($projects, drupal_map_assoc($project_names));
+ }
+ return $projects;
+}
+
+/**
+ * Clears the projects cache.
+ */
+function l10n_update_clear_cache_projects() {
+ drupal_static('l10n_update_get_projects', array());
+}
+
+/**
+ * Loads cached translation sources containing current translation status.
+ *
+ * @param array $projects
+ * Array of project names. Defaults to all translatable projects.
+ * @param array $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ * Array of source objects. Keyed with :.
+ *
+ * @see l10n_update_source_build()
+ */
+function l10n_update_load_sources($projects = NULL, $langcodes = NULL) {
+ $sources = array();
+ $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+
+ // Load source data from l10n_update_status cache.
+ $status = l10n_update_get_status();
+
+ // Use only the selected projects and languages for update.
+ foreach($projects as $project) {
+ foreach ($langcodes as $langcode) {
+ $sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL;
+ }
+ }
+ return $sources;
+}
+
+/**
+ * Build translation sources.
+ *
+ * @param array $projects
+ * Array of project names. Defaults to all translatable projects.
+ * @param array $langcodes
+ * Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ * Array of source objects. Keyed by project name and language code.
+ *
+ * @see l10n_update_source_build()
+ */
+function l10n_update_build_sources($projects = array(), $langcodes = array()) {
+ $sources = array();
+ $projects = l10n_update_get_projects($projects);
+ $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
+
+ foreach ($projects as $project) {
+ foreach ($langcodes as $langcode) {
+ $source = l10n_update_source_build($project, $langcode);
+ $sources[$source->name][$source->langcode] = $source;
+ }
+ }
+ return $sources;
+}
+
+/**
+ * Checks whether a po file exists in the local filesystem.
+ *
+ * It will search in the directory set in the translation source. Which defaults
+ * to the "translations://" stream wrapper path. The directory may contain any
+ * valid stream wrapper.
+ *
+ * The "local" files property of the source object contains the definition of a
+ * po file we are looking for. The file name defaults to
+ * %project-%release.%language.po. Per project this value can be overridden
+ * using the server_pattern directive in the module's .info.yml file or by using
+ * hook_l10n_update_projects_alter().
+ *
+ * @param object $source
+ * Translation source object.
+ *
+ * @return stdClass
+ * Source file object of the po file, updated with:
+ * - "uri": File name and path.
+ * - "timestamp": Last updated time of the po file.
+ * FALSE if the file is not found.
+ *
+ * @see l10n_update_source_build()
+ */
+function l10n_update_source_check_file($source) {
+ if (isset($source->files[L10N_UPDATE_LOCAL])) {
+ $source_file = $source->files[L10N_UPDATE_LOCAL];
+ $directory = $source_file->directory;
+ $filename = '/' . preg_quote($source_file->filename) . '$/';
+
+ if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) {
+ $file = current($files);
+ $source_file->uri = $file->uri;
+ $source_file->timestamp = filemtime($file->uri);
+ return $source_file;
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * Builds abstract translation source.
+ *
+ * @param object $project
+ * Project object.
+ * @param string $langcode
+ * Language code.
+ * @param string $filename
+ * File name of translation file. May contain placeholders.
+ *
+ * @return object
+ * Source object:
+ * - "project": Project name.
+ * - "name": Project name (inherited from project).
+ * - "language": Language code.
+ * - "core": Core version (inherited from project).
+ * - "version": Project version (inherited from project).
+ * - "project_type": Project type (inherited from project).
+ * - "files": Array of file objects containing properties of local and remote
+ * translation files.
+ * Other processes can add the following properties:
+ * - "type": Most recent translation source found. L10N_UPDATE_REMOTE and
+ * L10N_UPDATE_LOCAL indicate available new translations,
+ * L10N_UPDATE_CURRENT indicate that the current translation is them
+ * most recent. "type" sorresponds with a key of the "files" array.
+ * - "timestamp": The creation time of the "type" translation (file).
+ * - "last_checked": The time when the "type" translation was last checked.
+ * The "files" array can hold file objects of type:
+ * L10N_UPDATE_LOCAL, L10N_UPDATE_REMOTE and
+ * L10N_UPDATE_CURRENT. Each contains following properties:
+ * - "type": The object type (L10N_UPDATE_LOCAL,
+ * L10N_UPDATE_REMOTE, etc. see above).
+ * - "project": Project name.
+ * - "langcode": Language code.
+ * - "version": Project version.
+ * - "uri": Local or remote file path.
+ * - "directory": Directory of the local po file.
+ * - "filename": File name.
+ * - "timestamp": Timestamp of the file.
+ * - "keep": TRUE to keep the downloaded file.
+ */
+function l10n_update_source_build($project, $langcode, $filename = NULL) {
+ // Create a source object with data of the project object.
+ $source = clone $project;
+ $source->project = $project->name;
+ $source->langcode = $langcode;
+ $source->type = '';
+ $source->timestamp = 0;
+ $source->last_checked = 0;
+
+ $filename = $filename ? $filename : variable_get('l10n_update_default_filename', L10N_UPDATE_DEFAULT_FILE_NAME);
+
+ // If the server_pattern contains a remote file path we will check for a
+ // remote file. The local version of this file will only be checked if a
+ // translations directory has been defined. If the server_pattern is a local
+ // file path we will only check for a file in the local file system.
+ $files = array();
+ if (_l10n_update_file_is_remote($source->server_pattern)) {
+ $files[L10N_UPDATE_REMOTE] = (object) array(
+ 'project' => $project->name,
+ 'langcode' => $langcode,
+ 'version' => $project->version,
+ 'type' => L10N_UPDATE_REMOTE,
+ 'filename' => l10n_update_build_server_pattern($source, basename($source->server_pattern)),
+ 'uri' => l10n_update_build_server_pattern($source, $source->server_pattern),
+ );
+ $files[L10N_UPDATE_LOCAL] = (object) array(
+ 'project' => $project->name,
+ 'langcode' => $langcode,
+ 'version' => $project->version,
+ 'type' => L10N_UPDATE_LOCAL,
+ 'filename' => l10n_update_build_server_pattern($source, $filename),
+ 'directory' => 'translations://',
+ );
+ $files[L10N_UPDATE_LOCAL]->uri = $files[L10N_UPDATE_LOCAL]->directory . $files[L10N_UPDATE_LOCAL]->filename;
+ }
+ else {
+ $files[L10N_UPDATE_LOCAL] = (object) array(
+ 'project' => $project->name,
+ 'langcode' => $langcode,
+ 'version' => $project->version,
+ 'type' => L10N_UPDATE_LOCAL,
+ 'filename' => l10n_update_build_server_pattern($source, basename($source->server_pattern)),
+ 'directory' => l10n_update_build_server_pattern($source, drupal_dirname($source->server_pattern)),
+ );
+ $files[L10N_UPDATE_LOCAL]->uri = $files[L10N_UPDATE_LOCAL]->directory . '/' . $files[L10N_UPDATE_LOCAL]->filename;
+ }
+ $source->files = $files;
+
+ // If this project+language is already translated, we add its status and
+ // update the current translation timestamp and last_updated time. If the
+ // project+language is not translated before, create a new record.
+ $history = l10n_update_get_file_history();
+ if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) {
+ $source->files[L10N_UPDATE_CURRENT] = $history[$project->name][$langcode];
+ $source->type = L10N_UPDATE_CURRENT;
+ $source->timestamp = $history[$project->name][$langcode]->timestamp;
+ $source->last_checked = $history[$project->name][$langcode]->last_checked;
+ }
+ else {
+ l10n_update_update_file_history($source);
+ }
+
+ return $source;
+}
+
+/**
+ * Build path to translation source, out of a server path replacement pattern.
+ *
+ * @param object $project
+ * Project object containing data to be inserted in the template.
+ * @param string $template
+ * String containing placeholders. Available placeholders:
+ * - "%project": Project name.
+ * - "%release": Project version.
+ * - "%core": Project core version.
+ * - "%language": Language code.
+ *
+ * @return string
+ * String with replaced placeholders.
+ */
+function l10n_update_build_server_pattern($project, $template) {
+ $variables = array(
+ '%project' => $project->name,
+ '%release' => $project->version,
+ '%core' => $project->core,
+ '%language' => isset($project->langcode) ? $project->langcode : '%language',
+ );
+ return strtr($template, $variables);
+}
+
+/**
+ * Populate a queue with project to check for translation updates.
+ */
+function l10n_update_cron_fill_queue() {
+ $updates = array();
+
+ // Determine which project+language should be updated.
+ $last = REQUEST_TIME - variable_get('l10n_update_check_frequency', '0') * 3600 * 24;
+ $query = db_select('l10n_update_file', 'f');
+ $query->join('l10n_update_project', 'p', 'p.name = f.project');
+ $query->condition('f.last_checked', $last, '<');
+ $query->fields('f', array('project', 'language'));
+ // Only currently installed / enabled components should be checked for.
+ $query->condition('p.status', 1);
+ $files = $query->execute()->fetchAll();
+ foreach ($files as $file) {
+ $updates[$file->project][] = $file->language;
+
+ // Update the last_checked timestamp of the project+language that will
+ // be checked for updates.
+ db_update('l10n_update_file')
+ ->fields(array('last_checked' => REQUEST_TIME))
+ ->condition('project', $file->project)
+ ->condition('language', $file->language)
+ ->execute();
+ }
+
+ // For each project+language combination a number of tasks are added to
+ // the queue.
+ if ($updates) {
+ module_load_include('fetch.inc', 'l10n_update');
+ $options = _l10n_update_default_update_options();
+ $queue = DrupalQueue::get('l10n_update', TRUE);
+
+ foreach ($updates as $project => $languages) {
+ $batch = l10n_update_batch_update_build(array($project), $languages, $options);
+ foreach ($batch['operations'] as $item) {
+ $queue->createItem($item);
+ }
+ }
+ }
+}
+
+/**
+ * Determine if a file is a remote file.
+ *
+ * @param string $uri
+ * The URI or URI pattern of the file.
+ *
+ * @return boolean
+ * TRUE if the $uri is a remote file.
+ */
+function _l10n_update_file_is_remote($uri) {
+ $scheme = file_uri_scheme($uri);
+ if ($scheme) {
+ return !drupal_realpath($scheme . '://');
+ }
+ return FALSE;
+}
+
+/**
+ * Compare two update sources, looking for the newer one.
+ *
+ * The timestamp property of the source objects are used to determine which is
+ * the newer one.
+ *
+ * @param object $source1
+ * Source object of the first translation source.
+ * @param object $source2
+ * Source object of available update.
+ *
+ * @return integer
+ * - "L10N_UPDATE_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
+ * is missing.
+ * - "L10N_UPDATE_SOURCE_COMPARE_EQ": $source1 == $source2 OR both
+ * $source1 and $source2 are missing.
+ * - "L10N_UPDATE_SOURCE_COMPARE_EQ": $source1 > $source2 OR $source2
+ * is missing.
+ */
+function _l10n_update_source_compare($source1, $source2) {
+ if (isset($source1->timestamp) && isset($source2->timestamp)) {
+ if ($source1->timestamp == $source2->timestamp) {
+ return L10N_UPDATE_SOURCE_COMPARE_EQ;
+ }
+ else {
+ return $source1->timestamp > $source2->timestamp ? L10N_UPDATE_SOURCE_COMPARE_GT : L10N_UPDATE_SOURCE_COMPARE_LT;
+ }
+ }
+ elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
+ return L10N_UPDATE_SOURCE_COMPARE_GT;
+ }
+ elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
+ return L10N_UPDATE_SOURCE_COMPARE_LT;
+ }
+ else {
+ return L10N_UPDATE_SOURCE_COMPARE_EQ;
+ }
+}
+
+/**
+ * Returns default import options for translation update.
+ *
+ * @return array
+ * Array of translation import options.
+ */
+function _l10n_update_default_update_options() {
+ $options = array(
+ 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
+ 'finish_feedback' => TRUE,
+ 'use_remote' => l10n_update_use_remote_source(),
+ );
+
+ switch (variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP)) {
+ case LOCALE_IMPORT_OVERWRITE:
+ $options['overwrite_options'] = array(
+ 'customized' => TRUE,
+ 'not_customized' => TRUE,
+ );
+ break;
+ case L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED:
+ $options['overwrite_options'] = array(
+ 'customized' => FALSE,
+ 'not_customized' => TRUE,
+ );
+ break;
+ case LOCALE_IMPORT_KEEP:
+ $options['overwrite_options'] = array(
+ 'customized' => FALSE,
+ 'not_customized' => FALSE,
+ );
+ break;
+ }
+
+ return $options;
+}
+
+/**
+ * Import one string into the database.
+ *
+ * @param $report
+ * Report array summarizing the number of changes done in the form:
+ * array(inserts, updates, deletes).
+ * @param $langcode
+ * Language code to import string into.
+ * @param $context
+ * The context of this string.
+ * @param $source
+ * Source string.
+ * @param $translation
+ * Translation to language specified in $langcode.
+ * @param $textgroup
+ * Name of textgroup to store translation in.
+ * @param $location
+ * Location value to save with source string.
+ * @param $mode
+ * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
+ * @param $status
+ * Status of translation if created: L10N_UPDATE_STRING_DEFAULT or L10N_UPDATE_STRING_CUSTOM
+ * @param $plid
+ * Optional plural ID to use.
+ * @param $plural
+ * Optional plural value to use.
+ * @return
+ * The string ID of the existing string modified or the new string added.
+ */
+function _l10n_update_locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $status = L10N_UPDATE_NOT_CUSTOMIZED, $plid = 0, $plural = 0) {
+ $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField();
+
+ if (!empty($translation)) {
+ // Skip this string unless it passes a check for dangerous code.
+ // Text groups other than default still can contain HTML tags
+ // (i.e. translatable blocks).
+ if ($textgroup == "default" && !locale_string_is_safe($translation)) {
+ $report['skips']++;
+ $lid = 0;
+ watchdog('locale', 'Disallowed HTML detected. String not imported: %string', array('%string' => $translation), WATCHDOG_WARNING);
+ }
+ elseif ($lid) {
+ // We have this source string saved already.
+ db_update('locales_source')
+ ->fields(array(
+ 'location' => $location,
+ ))
+ ->condition('lid', $lid)
+ ->execute();
+
+ $exists = db_query("SELECT lid, l10n_status FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchObject();
+
+ if (!$exists) {
+ // No translation in this language.
+ db_insert('locales_target')
+ ->fields(array(
+ 'lid' => $lid,
+ 'language' => $langcode,
+ 'translation' => $translation,
+ 'plid' => $plid,
+ 'plural' => $plural,
+ ))
+ ->execute();
+
+ $report['additions']++;
+ }
+ elseif (($exists->l10n_status == L10N_UPDATE_NOT_CUSTOMIZED && $mode == L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED) || $mode == LOCALE_IMPORT_OVERWRITE) {
+ // Translation exists, only overwrite if instructed.
+ db_update('locales_target')
+ ->fields(array(
+ 'translation' => $translation,
+ 'plid' => $plid,
+ 'plural' => $plural,
+ ))
+ ->condition('language', $langcode)
+ ->condition('lid', $lid)
+ ->execute();
+
+ $report['updates']++;
+ }
+ }
+ else {
+ // No such source string in the database yet.
+ $lid = db_insert('locales_source')
+ ->fields(array(
+ 'location' => $location,
+ 'source' => $source,
+ 'context' => (string) $context,
+ 'textgroup' => $textgroup,
+ ))
+ ->execute();
+
+ db_insert('locales_target')
+ ->fields(array(
+ 'lid' => $lid,
+ 'language' => $langcode,
+ 'translation' => $translation,
+ 'plid' => $plid,
+ 'plural' => $plural,
+ 'l10n_status' => $status,
+ ))
+ ->execute();
+
+ $report['additions']++;
+ }
+ }
+ elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
+ // Empty translation, remove existing if instructed.
+ db_delete('locales_target')
+ ->condition('language', $langcode)
+ ->condition('lid', $lid)
+ ->condition('plid', $plid)
+ ->condition('plural', $plural)
+ ->execute();
+
+ $report['deletes']++;
+ }
+
+ return $lid;
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateCronTest.test b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateCronTest.test
new file mode 100644
index 00000000..2759286d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateCronTest.test
@@ -0,0 +1,115 @@
+ 'Update translations using cron',
+ 'description' => 'Tests for using cron to update project interface translations.',
+ 'group' => 'Localization Update',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ $admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
+ $this->drupalLogin($admin_user);
+ $this->addLanguage('de');
+ }
+
+ /**
+ * Tests interface translation update using cron.
+ */
+ function testUpdateCron() {
+ // Set a flag to let the l10n_update_test module replace the project data
+ // with a set of test projects.
+ variable_set('l10n_update_test_projects_alter', TRUE);
+
+ // Setup local and remote translations files.
+ $this->setTranslationFiles();
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Update translations using batch to ensure a clean test starting point.
+ $this->drupalGet('admin/config/regional/translate/check');
+ $this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
+
+ // Store translation status for comparison.
+ $initial_history = l10n_update_get_file_history();
+
+ // Prepare for test: Simulate new translations being available.
+ // Change the last updated timestamp of a translation file.
+ $contrib_module_two_uri = 'public://local/contrib_module_two-7.x-2.0-beta4.de._po';
+ touch(drupal_realpath($contrib_module_two_uri), REQUEST_TIME);
+
+ // Prepare for test: Simulate that the file has not been checked for a long
+ // time. Set the last_check timestamp to zero.
+ $query = db_update('l10n_update_file');
+ $query->fields(array('last_checked' => 0));
+ $query->condition('project', 'contrib_module_two');
+ $query->condition('language', 'de');
+ $query->execute();
+
+ // Test: Disable cron update and verify that no tasks are added to the
+ // queue.
+ $edit = array(
+ 'l10n_update_check_frequency' => '0',
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Execute l10n_update cron taks to add tasks to the queue.
+ l10n_update_cron();
+
+ // Check whether no tasks are added to the queue.
+ $queue = DrupalQueue::get('l10n_update', TRUE);
+ $this->assertEqual($queue->numberOfItems(), 0, 'Queue is empty');
+
+ // Test: Enable cron update and check if update tasks are added to the
+ // queue.
+ // Set cron update to Weekly.
+ $edit = array(
+ 'l10n_update_check_frequency' => '7',
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Execute l10n_update cron task to add tasks to the queue.
+ l10n_update_cron();
+
+ // Check whether tasks are added to the queue.
+ $queue = DrupalQueue::get('l10n_update', TRUE);
+ $this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
+ $item = $queue->claimItem();
+ $queue->releaseItem($item);
+ $this->assertEqual($item->data[1][0], 'contrib_module_two', 'Queue holds tasks for contrib module one.');
+
+ // Test: Run cron for a second time and check if tasks are not added to
+ // the queue twice.
+ l10n_update_cron();
+
+ // Check whether no more tasks are added to the queue.
+ $queue = DrupalQueue::get('l10n_update', TRUE);
+ $this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
+
+ // Ensure last checked is updated to a greater time than the initial value.
+ sleep(1);
+ // Test: Execute cron and check if tasks are executed correctly.
+ // Run cron to process the tasks in the queue.
+ $this->drupalGet('admin/reports/status/run-cron');
+
+ drupal_static_reset('l10n_update_get_file_history');
+ $history = l10n_update_get_file_history();
+ $initial = $initial_history['contrib_module_two']['de'];
+ $current = $history['contrib_module_two']['de'];
+ $this->assertTrue($current->timestamp > $initial->timestamp, 'Timestamp is updated');
+ $this->assertTrue($current->last_checked > $initial->last_checked, 'Last checked is updated');
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateInterfaceTest.test b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateInterfaceTest.test
new file mode 100644
index 00000000..4ce23ab5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateInterfaceTest.test
@@ -0,0 +1,91 @@
+ 'Update translations user interface',
+ 'description' => 'Tests for the user interface of project interface translations.',
+ 'group' => 'Localization Update',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ $admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Tests the user interfaces of the interface translation update system.
+ *
+ * Testing the Available updates summary on the side wide status page and the
+ * Avaiable translation updates page.
+ */
+ function testInterface() {
+ // Enable the module this test uses for its translations.
+ module_enable(array('l10n_update_test_translate'));
+
+ // No language added.
+ // Check status page and Available translation updates page.
+ $this->drupalGet('admin/reports/status');
+ $this->assertNoText(t('Translation update status'), 'No status message');
+
+ $this->drupalGet('admin/config/regional/translate/update');
+ $this->assertRaw(t('No translatable languages available. Add a language first.', array('@add_language' => url('admin/config/regional/language'))), 'Language message');
+
+ // Add German language.
+ $this->addLanguage('de');
+
+ // Drupal core is probably in 7.x, but tests may also be executed with
+ // stable releases. As this is an uncontrolled factor in the test, we will
+ // mark Drupal core as translated and continue with the prepared modules.
+ $status = l10n_update_get_status();
+ $status['drupal']['de']->type = 'current';
+ variable_set('l10n_update_translation_status', $status);
+
+ // One language added, all translations up to date.
+ $this->drupalGet('admin/reports/status');
+ $this->assertText(t('Translation update status'), 'Status message');
+ $this->assertText(t('Up to date'), 'Translations up to date');
+ $this->drupalGet('admin/config/regional/translate/update');
+ $this->assertText(t('All translations up to date.'), 'Translations up to date');
+
+ // Set l10n_update_test_translate module to have a local translation available.
+ $status = l10n_update_get_status();
+ $status['l10n_update_test_translate']['de']->type = 'local';
+ variable_set('l10n_update_translation_status', $status);
+
+ // Check if updates are available for German.
+ $this->drupalGet('admin/reports/status');
+ $this->assertText(t('Translation update status'), 'Status message');
+ $this->assertRaw(t('Updates available for: @languages. See the Available translation updates page for more information.', array('@languages' => t('German'), '@updates' => url('admin/config/regional/translate/update'))), 'Updates available message');
+ $this->drupalGet('admin/config/regional/translate/update');
+ $this->assertText(t('Updates for: @modules', array('@modules' => 'Localization Update test translate')), 'Translations avaiable');
+
+ // Set l10n_update_test_translate module to have a dev release and no
+ // translation found.
+ $status = l10n_update_get_status();
+ $status['l10n_update_test_translate']['de']->version = '1.3-dev';
+ $status['l10n_update_test_translate']['de']->type = '';
+ variable_set('l10n_update_translation_status', $status);
+
+ // Check if no updates were found.
+ $this->drupalGet('admin/reports/status');
+ $this->assertText(t('Translation update status'), 'Status message');
+ $this->assertRaw(t('Missing translations for: @languages. See the Available translation updates page for more information.', array('@languages' => t('German'), '@updates' => url('admin/config/regional/translate/update'))), 'Missing translations message');
+ $this->drupalGet('admin/config/regional/translate/update');
+ $this->assertText(t('Missing translations for one project'), 'No translations found');
+ $this->assertText(t('@module (@version).', array('@module' => 'Localization Update test translate', '@version' => '1.3-dev')), 'Release details');
+ $this->assertText(t('No translation files are provided for development releases.'), 'Release info');
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateTest.test b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateTest.test
new file mode 100644
index 00000000..c19409ba
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateTest.test
@@ -0,0 +1,440 @@
+ 'Update translations',
+ 'description' => 'Tests for updating the interface translations of projects.',
+ 'group' => 'Localization Update',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+
+ $admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
+ $this->drupalLogin($admin_user);
+
+ // We use German as test language. This language must match the translation
+ // file that come with the l10n_update_test module (test.de.po) and can therefore
+ // not be chosen randomly.
+ $this->addLanguage('de');
+
+ module_load_include('compare.inc', 'l10n_update');
+ module_load_include('fetch.inc', 'l10n_update');
+ }
+
+ /**
+ * Checks if a list of translatable projects gets build.
+ */
+ function testUpdateProjects() {
+ module_load_include('compare.inc', 'l10n_update');
+ variable_set('l10n_update_test_projects_alter', TRUE);
+
+ // Make the test modules look like a normal custom module. i.e. make the
+ // modules not hidden. l10n_update_test_system_info_alter() modifies the project
+ // info of the l10n_update_test and l10n_update_test_translate modules.
+ variable_set('l10n_update_test_system_info_alter', TRUE);
+ $this->resetAll();
+
+ // Check if interface translation data is collected from hook_info.
+ $projects = l10n_update_project_list();
+ $this->assertFalse(isset($projects['l10n_update_test_translate']), 'Hidden module not found');
+ $this->assertEqual($projects['l10n_update_test']['info']['interface translation server pattern'], 'sites/all/modules/l10n_update/tests/test.%language.po', 'Interface translation parameter found in project info.');
+ $this->assertEqual($projects['l10n_update_test']['name'] , 'l10n_update_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
+ }
+
+ /**
+ * Checks if local or remote translation sources are detected.
+ *
+ * The translation status process by default checks the status of the
+ * installed projects. For testing purpose a predefined set of modules with
+ * fixed file names and release versions is used. This custom project
+ * definition is applied using a hook_l10n_update_projects_alter
+ * implementation in the l10n_update_test module.
+ *
+ * This test generates a set of local and remote translation files in their
+ * respective local and remote translation directory. The test checks whether
+ * the most recent files are selected in the different check scenarios: check
+ * for local files only, check for both local and remote files.
+ */
+ function testUpdateCheckStatus() {
+ // Set a flag to let the l10n_update_test module replace the project data with a
+ // set of test projects.
+ variable_set('l10n_update_test_projects_alter', TRUE);
+
+ // Create local and remote translations files.
+ $this->setTranslationFiles();
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Set the test conditions.
+ $edit = array(
+ 'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_LOCAL,
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Get status of translation sources at local file system.
+ $this->drupalGet('admin/config/regional/translate/check');
+ $result = l10n_update_get_status();
+ $this->assertEqual($result['contrib_module_one']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_one found');
+ $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_old, 'Translation timestamp found');
+ $this->assertEqual($result['contrib_module_two']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_two found');
+ $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
+ $this->assertEqual($result['l10n_update_test']['de']->type, L10N_UPDATE_LOCAL, 'Translation of l10n_update_test found');
+ $this->assertEqual($result['custom_module_one']['de']->type, L10N_UPDATE_LOCAL, 'Translation of custom_module_one found');
+
+ // Set the test conditions.
+ $edit = array(
+ 'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Get status of translation sources at both local and remote locations.
+ $this->drupalGet('admin/config/regional/translate/check');
+ $result = l10n_update_get_status();
+ $this->assertEqual($result['contrib_module_one']['de']->type, L10N_UPDATE_REMOTE, 'Translation of contrib_module_one found');
+ $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
+ $this->assertEqual($result['contrib_module_two']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_two found');
+ $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
+ $this->assertEqual($result['contrib_module_three']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_three found');
+ $this->assertEqual($result['contrib_module_three']['de']->timestamp, $this->timestamp_old, 'Translation timestamp found');
+ $this->assertEqual($result['l10n_update_test']['de']->type, L10N_UPDATE_LOCAL, 'Translation of l10n_update_test found');
+ $this->assertEqual($result['custom_module_one']['de']->type, L10N_UPDATE_LOCAL, 'Translation of custom_module_one found');
+ }
+
+ /**
+ * Tests translation import from remote sources.
+ *
+ * Test conditions:
+ * - Source: remote and local files
+ * - Import overwrite: all existing translations
+ */
+ function testUpdateImportSourceRemote() {
+ // Build the test environment.
+ $this->setTranslationFiles();
+ $this-> setCurrentTranslations();
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Set the update conditions for this test.
+ $edit = array(
+ 'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
+ 'overwrite' => LOCALE_IMPORT_OVERWRITE,
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Get the translation status.
+ $this->drupalGet('admin/config/regional/translate/check');
+
+ // Check the status on the Available translation status page.
+ $this->assertRaw('', 'German language found');
+ $this->assertText('Updates for: Contributed module one, Contributed module two, Custom module one, Locale test', 'Updates found');
+ $this->assertText('Contributed module one (' . format_date($this->timestamp_new, 'medium') . ')', 'Updates for Contrib module one');
+ $this->assertText('Contributed module two (' . format_date($this->timestamp_new, 'medium') . ')', 'Updates for Contrib module two');
+
+ // Execute the translation update.
+ $this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
+
+ // Check if the translation has been updated, using the status cache.
+ $status = l10n_update_get_status();
+ $this->assertEqual($status['contrib_module_one']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_one found');
+ $this->assertEqual($status['contrib_module_two']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_two found');
+ $this->assertEqual($status['contrib_module_three']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_three found');
+
+ // Check the new translation status.
+ // The static cache needs to be flushed first to get the most recent data
+ // from the database. The function was called earlier during this test.
+ drupal_static_reset('l10n_update_get_file_history');
+ $history = l10n_update_get_file_history();
+ $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported');
+ $this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated');
+ $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported');
+ $this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated');
+ $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
+ $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
+
+ // Check whether existing translations have (not) been overwritten.
+ $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January');
+ $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
+ $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
+ $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
+ $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+ $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+ $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+ }
+
+ /**
+ * Tests translation import from local sources.
+ *
+ * Test conditions:
+ * - Source: local files only
+ * - Import overwrite: all existing translations
+ */
+ function testUpdateImportSourceLocal() {
+ // Build the test environment.
+ $this->setTranslationFiles();
+ $this-> setCurrentTranslations();
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Set the update conditions for this test.
+ $edit = array(
+ 'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_LOCAL,
+ 'overwrite' => LOCALE_IMPORT_OVERWRITE,
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Execute the translation update.
+ $this->drupalGet('admin/config/regional/translate/check');
+ $this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
+
+ // Check if the translation has been updated, using the status cache.
+ $status = l10n_update_get_status();
+ $this->assertEqual($status['contrib_module_one']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_one found');
+ $this->assertEqual($status['contrib_module_two']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_two found');
+ $this->assertEqual($status['contrib_module_three']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_three found');
+
+ // Check the new translation status.
+ // The static cache needs to be flushed first to get the most recent data
+ // from the database. The function was called earlier during this test.
+ drupal_static_reset('l10n_update_get_file_history');
+ $history = l10n_update_get_file_history();
+ $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_medium, 'Translation of contrib_module_one is imported');
+ $this->assertEqual($history['contrib_module_one']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_one is updated');
+ $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported');
+ $this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated');
+ $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
+ $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
+
+ // Check whether existing translations have (not) been overwritten.
+ $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
+ $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
+ $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
+ $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
+ $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+ $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+ $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+ }
+
+ /**
+ * Tests translation import and only overwrite non-customized translations.
+ *
+ * Test conditions:
+ * - Source: remote and local files
+ * - Import overwrite: only overwrite non-customized translations
+ */
+ function testUpdateImportModeNonCustomized() {
+ // Build the test environment.
+ $this->setTranslationFiles();
+ $this-> setCurrentTranslations();
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Set the test conditions.
+ $edit = array(
+ 'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
+ 'overwrite' => L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED,
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Execute translation update.
+ $this->drupalGet('admin/config/regional/translate/check');
+ $this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
+
+ // Check whether existing translations have (not) been overwritten.
+ $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
+ $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_customized', 'Translation of February');
+ $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
+ $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
+ $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+ $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+ $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+ }
+
+ /**
+ * Tests translation import and don't overwrite any translation.
+ *
+ * Test conditions:
+ * - Source: remote and local files
+ * - Import overwrite: don't overwrite any existing translation
+ */
+ function testUpdateImportModeNone() {
+ // Build the test environment.
+ $this->setTranslationFiles();
+ $this-> setCurrentTranslations();
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Set the test conditions.
+ $edit = array(
+ 'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
+ 'overwrite' => LOCALE_IMPORT_KEEP,
+ );
+ $this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
+
+ // Execute translation update.
+ $this->drupalGet('admin/config/regional/translate/check');
+ $this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
+
+ // Check whether existing translations have (not) been overwritten.
+ $this->assertTranslation('January', 'Januar_customized', 'de');
+ $this->assertTranslation('February', 'Februar_customized', 'de');
+ $this->assertTranslation('March', 'Marz', 'de');
+ $this->assertTranslation('April', 'April_2', 'de');
+ $this->assertTranslation('May', 'Mai_customized', 'de');
+ $this->assertTranslation('June', 'Juni', 'de');
+ $this->assertTranslation('Monday', 'Montag', 'de');
+ }
+
+ /**
+ * Tests automatic translation import when a module is enabled.
+ */
+ function testEnableUninstallModule() {
+ // Make the hidden test modules look like a normal custom module.
+ variable_set('l10n_update_test_system_info_alter', TRUE);
+
+ // Check if there is no translation yet.
+ $this->assertTranslation('Tuesday', '', 'de');
+
+ // Enable a module.
+ $edit = array(
+ 'modules[Testing][l10n_update_test_translate][enable]' => '1',
+ );
+ $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+
+ // Check if translations have been imported.
+ $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+ array('%number' => 0, '%update' => 7, '%delete' => 0)), 'One translation file imported.');
+ $this->assertTranslation('Tuesday', 'Dienstag', 'de');
+
+// // Disable and uninstall a module
+// module_disable(array('l10n_update_test_translate'));
+// $edit = array(
+// 'uninstall[l10n_update_test_translate]' => '1',
+// );
+// $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall'));
+// $this->drupalPost(NULL, array(), t('Uninstall'));
+//
+// // Check if the file data is removed from the database.
+// $history = l10n_update_get_file_history();
+// $this->assertFalse(isset($history['l10n_update_test_translate']), 'Project removed from the file history');
+// $projects = l10n_update_get_projects();
+// $this->assertFalse(isset($projects['l10n_update_test_translate']), 'Project removed from the project list');
+ }
+
+ /**
+ * Tests automatic translation import when a langauge is enabled.
+ *
+ * When a language is added, the system will check for translations files of
+ * enabled modules and will import them. When a language is removed the system
+ * will remove all translations of that langugue from the database.
+ */
+ function testEnableLanguage() {
+ // Make the hidden test modules look like a normal custom module.
+ variable_set('l10n_update_test_system_info_alter', TRUE);
+
+ // Enable a module.
+ $edit = array(
+ 'modules[Testing][l10n_update_test_translate][enable]' => '1',
+ );
+ $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+
+ // Check if there is no Dutch translation yet.
+ $this->assertTranslation('Extraday', '', 'nl');
+ $this->assertTranslation('Tuesday', 'Dienstag', 'de');
+
+ // Add a language.
+ $this->addLanguage('nl');
+
+ // Check if the right number of translations are added.
+ $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+ array('%number' => 0, '%update' => 8, '%delete' => 0)), 'One language added.');
+ $this->assertTranslation('Extraday', 'extra dag', 'nl');
+
+ // Check if the language data is added to the database.
+ $result = db_query("SELECT project FROM {l10n_update_file} WHERE language='nl'")->fetchField();
+ $this->assertTrue((boolean) $result, 'Files removed from file history');
+
+ // Remove a language.
+ $this->drupalPost('admin/config/regional/language/delete/nl', array(), t('Delete'));
+
+ // Check if the language data is removed from the database.
+ $result = db_query("SELECT project FROM {l10n_update_file} WHERE language='nl'")->fetchField();
+ $this->assertFalse($result, 'Files removed from file history');
+
+ // Check that the Dutch translation is gone.
+ $this->assertTranslation('Extraday', '', 'nl');
+ $this->assertTranslation('Tuesday', 'Dienstag', 'de');
+ }
+
+ /**
+ * Tests automatic translation import when a custom langauge is enabled.
+ */
+ function testEnableCustomLanguage() {
+ // Make the hidden test modules look like a normal custom module.
+ variable_set('l10n_update_test_system_info_alter', TRUE);
+
+ // Enable a module.
+ $edit = array(
+ 'modules[Testing][l10n_update_test_translate][enable]' => '1',
+ );
+ $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+
+ // Create and enable a custom language with language code 'xx' and a random
+ // name.
+ $langcode = 'xx';
+ $name = $this->randomName(16);
+ $edit = array(
+ 'langcode' => $langcode,
+ 'name' => $name,
+ 'native' => $name,
+ 'prefix' => $langcode,
+ 'direction' => '0',
+ );
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
+ drupal_static_reset('language_list');
+ $languages = language_list();
+ $this->assertTrue(isset($languages[$langcode]), format_string('Language %langcode added.', array('%langcode' => $langcode)));
+
+ // Ensure the translation file is automatically imported when the language
+ // was added.
+ $this->assertText(t('One translation file imported.'), 'Language file automatically imported.');
+ $this->assertText(t('One translation string was skipped because of disallowed or malformed HTML'), 'Language file automatically imported.');
+
+ // Ensure the strings were successfully imported.
+ $search = array(
+ 'string' => 'lundi',
+ 'language' => $langcode,
+ 'translation' => 'translated',
+ );
+ $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+ $this->assertNoText(t('No strings available.'), 'String successfully imported.');
+
+ // Ensure the multiline string was imported.
+ $search = array(
+ 'string' => 'Source string for multiline translation',
+ 'language' => $langcode,
+ 'translation' => 'all',
+ );
+ $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+ $this->assertText('Source string for multiline translation', 'String successfully imported.');
+
+ // Ensure 'Allowed HTML source string' was imported but the translation for
+ // 'Another allowed HTML source string' was not because it contains invalid
+ // HTML.
+ $search = array(
+ 'string' => 'HTML source string',
+ 'language' => $langcode,
+ 'translation' => 'translated',
+ );
+ $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+// $this->assertText('Allowed HTML source string', 'String successfully imported.');
+ $this->assertNoText('Another allowed HTML source string', 'String with disallowed translation not imported.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateTestBase.test b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateTestBase.test
new file mode 100644
index 00000000..2738701e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/L10nUpdateTestBase.test
@@ -0,0 +1,284 @@
+timestamp_old = REQUEST_TIME - 300;
+ $this->timestamp_medium = REQUEST_TIME - 200;
+ $this->timestamp_new = REQUEST_TIME - 100;
+ $this->timestamp_now = REQUEST_TIME;
+ }
+
+ /**
+ * Sets the value of the default translations directory.
+ *
+ * @param string $path
+ * Path of the translations directory relative to the drupal installation
+ * directory.
+ */
+ protected function setTranslationsDirectory($path) {
+ file_prepare_directory($path, FILE_CREATE_DIRECTORY);
+ variable_set('l10n_update_download_store', $path);
+ }
+
+ /**
+ * Adds a language.
+ *
+ * @param $langcode
+ * The language code of the language to add.
+ */
+ protected function addLanguage($langcode) {
+ $edit = array('langcode' => $langcode);
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+ drupal_static_reset('language_list');
+ $languages = language_list();
+ $this->assertTrue(isset($languages[$langcode]), format_string('Language %langcode added.', array('%langcode' => $langcode)));
+ }
+
+ /**
+ * Creates a translation file and tests its timestamp.
+ *
+ * @param string $path
+ * Path of the file relative to the public file path.
+ * @param string $filename
+ * Name of the file to create.
+ * @param integer $timestamp
+ * Timestamp to set the file to. Defaults to current time.
+ * @param array $translations
+ * Array of source/target value translation strings. Only singular strings
+ * are supported, no plurals. No double quotes are allowed in source and
+ * translations strings.
+ */
+ protected function makePoFile($path, $filename, $timestamp = NULL, $translations = array()) {
+ $timestamp = $timestamp ? $timestamp : REQUEST_TIME;
+ $path = 'public://' . $path;
+ $text = '';
+ $po_header = << 1);\\n"
+
+EOF;
+
+ // Convert array of translations to Gettext source and translation strings.
+ if ($translations) {
+ foreach ($translations as $source => $target) {
+ $text .= 'msgid "'. $source . '"' . "\n";
+ $text .= 'msgstr "'. $target . '"' . "\n";
+ }
+ }
+
+ file_prepare_directory($path, FILE_CREATE_DIRECTORY);
+ $file = (object) array(
+ 'uid' => 1,
+ 'filename' => $filename,
+ 'uri' => $path . '/' . $filename,
+ 'filemime' => 'text/x-gettext-translation',
+ 'timestamp' => $timestamp,
+ 'status' => FILE_STATUS_PERMANENT,
+ );
+ file_put_contents($file->uri, $po_header . $text);
+ touch(drupal_realpath($file->uri), $timestamp);
+ file_save($file);
+ }
+
+ /**
+ * Setup the environment containing local and remote translation files.
+ *
+ * Update tests require a simulated environment for local and remote files.
+ * Normally remote files are located at a remote server (e.g. ftp.drupal.org).
+ * For testing we can not rely on this. A directory in the file system of the
+ * test site is designated for remote files and is addressed using an absolute
+ * URL. Because Drupal does not allow files with a po extension to be accessed
+ * (denied in .htaccess) the translation files get a _po extension. Another
+ * directory is designated for local translation files.
+ *
+ * The environment is set up with the following files. File creation times are
+ * set to create different variations in test conditions.
+ * contrib_module_one
+ * - remote file: timestamp new
+ * - local file: timestamp old
+ * - current: timestamp medium
+ * contrib_module_two
+ * - remote file: timestamp old
+ * - local file: timestamp new
+ * - current: timestamp medium
+ * contrib_module_three
+ * - remote file: timestamp old
+ * - local file: timestamp old
+ * - current: timestamp medium
+ * custom_module_one
+ * - local file: timestamp new
+ * - current: timestamp medium
+ * Time stamp of current translation set by setCurrentTranslations() is always
+ * timestamp medium. This makes it easy to predict which translation will be
+ * imported.
+ */
+ protected function setTranslationFiles() {
+ // A flag is set to let the l10n_update_test module replace the project data with
+ // a set of test projects which match the below project files.
+ variable_set('l10n_update_test_projects_alter', TRUE);
+
+ // Setup the environment.
+ $public_path = drupal_realpath('public://');
+ $this->setTranslationsDirectory($public_path . '/local');
+ variable_set('l10n_update_default_filename', '%project-%release.%language._po');
+
+ // Setting up sets of translations for the translation files.
+ $translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1');
+ $translations_two = array( 'February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2');
+ $translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3');
+
+ // Add a number of files to the local file system to serve as remote
+ // translation server and match the project definitions set in
+ // l10n_update_test_l10n_update_projects_alter().
+ $this->makePoFile('remote/7.x/contrib_module_one', 'contrib_module_one-7.x-1.1.de._po', $this->timestamp_new, $translations_one);
+ $this->makePoFile('remote/7.x/contrib_module_two', 'contrib_module_two-7.x-2.0-beta4.de._po', $this->timestamp_old, $translations_two);
+ $this->makePoFile('remote/7.x/contrib_module_three', 'contrib_module_three-7.x-1.0.de._po', $this->timestamp_old, $translations_three);
+
+ // Add a number of files to the local file system to serve as local
+ // translation files and match the project definitions set in
+ // l10n_update_test_l10n_update_projects_alter().
+ $this->makePoFile('local', 'contrib_module_one-7.x-1.1.de._po', $this->timestamp_old, $translations_one);
+ $this->makePoFile('local', 'contrib_module_two-7.x-2.0-beta4.de._po', $this->timestamp_new, $translations_two);
+ $this->makePoFile('local', 'contrib_module_three-7.x-1.0.de._po', $this->timestamp_old, $translations_three);
+ $this->makePoFile('local', 'custom_module_one.de.po', $this->timestamp_new);
+ }
+
+ /**
+ * Setup existing translations in the database and set up the status of
+ * existing translations.
+ */
+ protected function setCurrentTranslations() {
+ // Setup to add German translations to the database.
+ $langcode = 'de';
+ $writer = new PoDatabaseWriter();
+ $writer->setLangcode($langcode);
+ $writer->setOptions(array(
+ 'overwrite_options' => array(
+ 'not_customized' => TRUE,
+ 'customized' => TRUE,
+ ),
+ ));
+
+ // Add non customized translations to the database.
+ $writer->setOptions(array('customized' => L10N_UPDATE_NOT_CUSTOMIZED));
+ $non_customized_translations = array(
+ 'March' => 'Marz',
+ 'June' => 'Juni',
+ );
+
+ foreach ($non_customized_translations as $source => $translation) {
+ $poItem = new PoItem();
+ $poItem->setFromArray(array(
+ 'source' => $source,
+ 'translation' => $translation,
+ ));
+ $writer->writeItem($poItem);
+ }
+
+ // Add customized translations to the database.
+ $writer->setOptions(array('customized' => L10N_UPDATE_CUSTOMIZED));
+ $customized_translations = array(
+ 'January' => 'Januar_customized',
+ 'February' => 'Februar_customized',
+ 'May' => 'Mai_customized',
+ );
+
+ foreach ($customized_translations as $source => $translation) {
+ $poItem = new PoItem();
+ $poItem->setFromArray(array(
+ 'source' => $source,
+ 'translation' => $translation,
+ ));
+ $writer->writeItem($poItem);
+ }
+
+ // Add a state of current translations in l10n_update_files.
+ $default = array(
+ 'language' => $langcode,
+ 'uri' => '',
+ 'timestamp' => $this->timestamp_medium,
+ 'last_checked' => $this->timestamp_medium,
+ );
+ $data[] = array(
+ 'project' => 'contrib_module_one',
+ 'filename' => 'contrib_module_one-7.x-1.1.de._po',
+ 'version' => '7.x-1.1',
+ );
+ $data[] = array(
+ 'project' => 'contrib_module_two',
+ 'filename' => 'contrib_module_two-7.x-2.0-beta4.de._po',
+ 'version' => '7.x-2.0-beta4',
+ );
+ $data[] = array(
+ 'project' => 'contrib_module_three',
+ 'filename' => 'contrib_module_three-7.x-1.0.de._po',
+ 'version' => '7.x-1.0',
+ );
+ $data[] = array(
+ 'project' => 'custom_module_one',
+ 'filename' => 'custom_module_one.de.po',
+ 'version' => '',
+ );
+ foreach ($data as $file) {
+ $file = (object) array_merge($default, $file);
+ drupal_write_record('l10n_update_file', $file);
+ }
+ }
+
+ /**
+ * Checks the translation of a string.
+ *
+ * @param string $source
+ * Translation source string
+ * @param string $translation
+ * Translation to check. Use empty string to check for a not existing
+ * translation.
+ * @param string $langcode
+ * Language code of the language to translate to.
+ * @param string $message
+ * (optional) A message to display with the assertion.
+ */
+ protected function assertTranslation($source, $translation, $langcode, $message = '') {
+ $db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField();
+ $db_translation = $db_translation == FALSE ? '' : $db_translation;
+ $this->assertEqual($translation, $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode)));
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test/l10n_update_test.info b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test/l10n_update_test.info
new file mode 100644
index 00000000..7dac6a76
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test/l10n_update_test.info
@@ -0,0 +1,14 @@
+name = 'Localization Update test'
+type = module
+description = 'Support module for Localization Update module testing.'
+package = Testing
+version = '1.2'
+core = 7.x
+hidden = true
+
+; Information added by Drupal.org packaging script on 2014-11-10
+version = "7.x-2.0"
+core = "7.x"
+project = "l10n_update"
+datestamp = "1415625781"
+
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test/l10n_update_test.install b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test/l10n_update_test.install
new file mode 100644
index 00000000..e7c2ff76
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test/l10n_update_test.install
@@ -0,0 +1,15 @@
+name == 'l10n_update_test' || $file->name == 'l10n_update_test_translate') {
+ // Don't hide the module.
+ $info['hidden'] = FALSE;
+ }
+ }
+}
+
+/**
+ * Implements hook_l10n_update_projects_alter().
+ *
+ * The translation status process by default checks the status of the installed
+ * projects. This function replaces the data of the installed modules by a
+ * predefined set of modules with fixed file names and release versions. Project
+ * names, versions, timestamps etc must be fixed because they must match the
+ * files created by the test script.
+ *
+ * The "l10n_update_test_projects_alter" variable must be set by the test script
+ * in order for this hook to take effect.
+ */
+function l10n_update_test_l10n_update_projects_alter(&$projects) {
+ if (variable_get('l10n_update_test_projects_alter', FALSE)) {
+
+ // Instead of the default ftp.drupal.org we use the file system of the test
+ // instance to simulate a remote file location.
+ $wrapper = file_stream_wrapper_get_instance_by_uri('public://');
+ $remote_url = $wrapper->getExternalUrl() . '/remote/';
+
+ // Completely replace the project data with a set of test projects.
+ $projects = array (
+ 'contrib_module_one' => array (
+ 'name' => 'contrib_module_one',
+ 'info' => array (
+ 'name' => 'Contributed module one',
+ 'l10n path' => $remote_url . '%core/%project/%project-%release.%language._po',
+ 'package' => 'Other',
+ 'version' => '7.x-1.1',
+ 'project' => 'contrib_module_one',
+ 'datestamp' => '1344471537',
+ '_info_file_ctime' => 1348767306,
+ ),
+ 'datestamp' => '1344471537',
+ 'includes' => array (
+ 'contrib_module_one' => 'Contributed module one',
+ ),
+ 'project_type' => 'module',
+ 'project_status' => TRUE,
+ ),
+ 'contrib_module_two' => array (
+ 'name' => 'contrib_module_two',
+ 'info' => array (
+ 'name' => 'Contributed module two',
+ 'l10n path' => $remote_url . '%core/%project/%project-%release.%language._po',
+ 'package' => 'Other',
+ 'version' => '7.x-2.0-beta4',
+ 'project' => 'contrib_module_two',
+ 'datestamp' => '1344471537',
+ '_info_file_ctime' => 1348767306,
+ ),
+ 'datestamp' => '1344471537',
+ 'includes' => array (
+ 'contrib_module_two' => 'Contributed module two',
+ ),
+ 'project_type' => 'module',
+ 'project_status' => TRUE,
+ ),
+ 'contrib_module_three' => array (
+ 'name' => 'contrib_module_three',
+ 'info' => array (
+ 'name' => 'Contributed module three',
+ 'l10n path' => $remote_url . '%core/%project/%project-%release.%language._po',
+ 'package' => 'Other',
+ 'version' => '7.x-1.0',
+ 'project' => 'contrib_module_three',
+ 'datestamp' => '1344471537',
+ '_info_file_ctime' => 1348767306,
+ ),
+ 'datestamp' => '1344471537',
+ 'includes' => array (
+ 'contrib_module_three' => 'Contributed module three',
+ ),
+ 'project_type' => 'module',
+ 'project_status' => TRUE,
+ ),
+ 'l10n_update_test' => array (
+ 'name' => 'l10n_update_test',
+ 'info' => array (
+ 'name' => 'Locale test',
+ 'interface translation project' => 'l10n_update_test',
+ 'l10n path' => 'sites/all/modules/l10n_update/tests/test.%language.po',
+ 'package' => 'Other',
+ 'version' => NULL,
+ 'project' => 'l10n_update_test',
+ '_info_file_ctime' => 1348767306,
+ 'datestamp' => 0,
+ ),
+ 'datestamp' => 0,
+ 'includes' => array (
+ 'l10n_update_test' => 'Locale test',
+ ),
+ 'project_type' => 'module',
+ 'project_status' => TRUE,
+ ),
+ 'custom_module_one' => array (
+ 'name' => 'custom_module_one',
+ 'info' => array (
+ 'name' => 'Custom module one',
+ 'interface translation project' => 'custom_module_one',
+ 'l10n path' => 'translations://custom_module_one.%language.po',
+ 'package' => 'Other',
+ 'version' => NULL,
+ 'project' => 'custom_module_one',
+ '_info_file_ctime' => 1348767306,
+ 'datestamp' => 0,
+ ),
+ 'datestamp' => 0,
+ 'includes' => array (
+ 'custom_module_one' => 'Custom module one',
+ ),
+ 'project_type' => 'module',
+ 'project_status' => TRUE,
+ ),
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/l10n_update_test_translate.info b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/l10n_update_test_translate.info
new file mode 100644
index 00000000..510f31fb
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/l10n_update_test_translate.info
@@ -0,0 +1,16 @@
+name = 'Localization Update test translate'
+type = module
+description = 'Translation test module for Localization Update module testing.'
+package = Testing
+version = '1.3'
+core = 7.x
+hidden = true
+interface translation project = l10n_update_test_translate
+l10n path = sites/all/modules/contrib/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.%language.po
+
+; Information added by Drupal.org packaging script on 2014-11-10
+version = "7.x-2.0"
+core = "7.x"
+project = "l10n_update"
+datestamp = "1415625781"
+
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/l10n_update_test_translate.module b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/l10n_update_test_translate.module
new file mode 100644
index 00000000..25b9ed87
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/l10n_update_test_translate.module
@@ -0,0 +1,28 @@
+name == 'l10n_update_test_translate') {
+ // Don't hide the module.
+ if (isset($info['hidden'])) {
+ $info['hidden'] = FALSE;
+ }
+
+ // Correct the path to the translation file. At a test-environment, the
+ // module may be place in a different path.
+ $basename = basename($info['l10n path']);
+ $path = drupal_get_path('module', 'l10n_update') . '/tests/modules/l10n_update_test_translate/translations/';
+ $info['l10n path'] = $path . $basename;
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.de.po b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.de.po
new file mode 100644
index 00000000..85e2841f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.de.po
@@ -0,0 +1,28 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 7\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "Monday"
+msgstr "Montag"
+
+msgid "Tuesday"
+msgstr "Dienstag"
+
+msgid "Wednesday"
+msgstr "Mittwoch"
+
+msgid "Thursday"
+msgstr "Donnerstag"
+
+msgid "Friday"
+msgstr "Freitag"
+
+msgid "Saturday"
+msgstr "Samstag"
+
+msgid "Sunday"
+msgstr "Sonntag"
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.nl.po b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.nl.po
new file mode 100644
index 00000000..2e956c19
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.nl.po
@@ -0,0 +1,31 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 7\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "Monday"
+msgstr "maandag"
+
+msgid "Tuesday"
+msgstr "dinsdag"
+
+msgid "Wednesday"
+msgstr "woensdag"
+
+msgid "Thursday"
+msgstr "donderdag"
+
+msgid "Extraday"
+msgstr "extra dag"
+
+msgid "Friday"
+msgstr "vrijdag"
+
+msgid "Saturday"
+msgstr "zaterdag"
+
+msgid "Sunday"
+msgstr "zondag"
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.xx.po b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.xx.po
new file mode 100644
index 00000000..e2db1d30
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.xx.po
@@ -0,0 +1,40 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 7\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "Monday"
+msgstr "lundi"
+
+msgid "Tuesday"
+msgstr "mardi"
+
+msgid "Wednesday"
+msgstr "mercredi"
+
+msgid "Thursday"
+msgstr "jeudi"
+
+msgid "Friday"
+msgstr "vendredi"
+
+msgid "Saturday"
+msgstr "samedi"
+
+msgid "Sunday"
+msgstr "dimanche"
+
+msgid "Allowed HTML source string"
+msgstr "Allowed HTML translation string"
+
+msgid "Another allowed HTML source string"
+msgstr ""
+
+msgid "Source string for multiline translation"
+msgstr ""
+"Multiline translation string "
+"to make sure that "
+"import works with it."
diff --git a/sites/all/modules/contrib/localisation/l10n_update/tests/test.de.po b/sites/all/modules/contrib/localisation/l10n_update/tests/test.de.po
new file mode 100644
index 00000000..67183fb6
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/l10n_update/tests/test.de.po
@@ -0,0 +1,10 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 7\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "Monday"
+msgstr "Montag"
diff --git a/sites/all/modules/contrib/mail/maillog/includes/maillog.mail.inc b/sites/all/modules/contrib/mail/maillog/includes/maillog.mail.inc
index 5967667d..ad30769d 100644
--- a/sites/all/modules/contrib/mail/maillog/includes/maillog.mail.inc
+++ b/sites/all/modules/contrib/mail/maillog/includes/maillog.mail.inc
@@ -14,7 +14,8 @@ class MaillogMailSystem implements MailSystemInterface {
* The formatted $message.
*/
public function format(array $message) {
- $default = new DefaultMailSystem();
+ $mailclass = variable_get('maillog_engine', 'DefaultMailSystem');
+ $default = new $mailclass();
return $default->format($message);
}
@@ -90,7 +91,8 @@ class MaillogMailSystem implements MailSystemInterface {
}
if (variable_get('maillog_send', TRUE)) {
- $default = new DefaultMailSystem();
+ $mailclass = variable_get('maillog_engine', 'DefaultMailSystem');
+ $default = new $mailclass();
$result = $default->mail($message);
}
elseif (user_access('administer maillog')) {
diff --git a/sites/all/modules/contrib/mail/maillog/includes/maillog.views_default.inc b/sites/all/modules/contrib/mail/maillog/includes/maillog.views_default.inc
index 3904fd2e..3d2e9891 100644
--- a/sites/all/modules/contrib/mail/maillog/includes/maillog.views_default.inc
+++ b/sites/all/modules/contrib/mail/maillog/includes/maillog.views_default.inc
@@ -218,7 +218,7 @@ $handler->override_option('cache', array(
));
$handler->override_option('title', 'Maillog');
$handler->override_option('empty', 'Maillog is currently empty. Send a mail!');
-$handler->override_option('empty_format', '1');
+$handler->override_option('empty_format', 'plain_text');
$handler->override_option('use_pager', '1');
$handler->override_option('style_plugin', 'table');
$handler = $view->new_display('page', 'Page', 'page_1');
diff --git a/sites/all/modules/contrib/mail/maillog/maillog.info b/sites/all/modules/contrib/mail/maillog/maillog.info
index d522aa0d..91de1594 100644
--- a/sites/all/modules/contrib/mail/maillog/maillog.info
+++ b/sites/all/modules/contrib/mail/maillog/maillog.info
@@ -11,9 +11,9 @@ files[] = includes/maillog.mail.inc
files[] = includes/maillog_handler_field_maillog_entry_link_delete.inc
files[] = includes/maillog_handler_field_maillog_link_delete.inc
-; Information added by drupal.org packaging script on 2012-02-13
-version = "7.x-1.x-dev"
+; Information added by Drupal.org packaging script on 2014-07-20
+version = "7.x-1.0-alpha1"
core = "7.x"
project = "maillog"
-datestamp = "1329135676"
+datestamp = "1405886928"
diff --git a/sites/all/modules/contrib/mail/maillog/maillog.module b/sites/all/modules/contrib/mail/maillog/maillog.module
index 3ed9ccc2..e4107fec 100644
--- a/sites/all/modules/contrib/mail/maillog/maillog.module
+++ b/sites/all/modules/contrib/mail/maillog/maillog.module
@@ -138,15 +138,15 @@ function maillog_admin_settings() {
'#title' => t("Display the e-mails on page using devel module (if enabled)."),
'#default_value' => variable_get('maillog_devel', TRUE),
);
- if (module_exists('mimemail')) {
- $engines = mimemail_get_engines();
+ if (module_exists('mailsystem')) {
+ $mailsystem_classes = mailsystem_get_classes();
// maillog will be unset, because ist would cause an recursion
- unset($engines['maillog']);
+ unset($mailsystem_classes['MaillogMailSystem']);
$form['maillog_engine'] = array(
'#type' => 'select',
- '#title' => t("Select the mailengine which should be used."),
- '#default_value' => variable_get('maillog_engine', 'mimemail'),
- '#options' => $engines,
+ '#title' => t("Select the mail system which should be used."),
+ '#default_value' => variable_get('maillog_engine', 'DefaultMailSystem'),
+ '#options' => $mailsystem_classes,
);
}
return system_settings_form($form);
diff --git a/sites/all/modules/contrib/mail/mandrill_simplenews_report/mandrill_simplenews_report.info b/sites/all/modules/contrib/mail/mandrill_simplenews_report/mandrill_simplenews_report.info
index 241fc48a..f88ed497 100644
--- a/sites/all/modules/contrib/mail/mandrill_simplenews_report/mandrill_simplenews_report.info
+++ b/sites/all/modules/contrib/mail/mandrill_simplenews_report/mandrill_simplenews_report.info
@@ -8,8 +8,8 @@ dependencies[] = mandrill
files[] = mandrill_simplenews_report.mandrill.inc
; Information added by Drupal.org packaging script on 2014-01-22
-version = "7.x-1.0-alpha+0-dev"
+version = "7.x-1.0-alpha1"
core = "7.x"
project = "mandrill_simplenews_report"
-datestamp = "1390384106"
+datestamp = "1390384405"
diff --git a/sites/all/modules/contrib/migrate/migrate/CHANGELOG.txt b/sites/all/modules/contrib/migrate/migrate/CHANGELOG.txt
index 0fd432c2..dc6ac96d 100644
--- a/sites/all/modules/contrib/migrate/migrate/CHANGELOG.txt
+++ b/sites/all/modules/contrib/migrate/migrate/CHANGELOG.txt
@@ -1,3 +1,235 @@
+Migrate 2.7
+===========
+
+Bug fixes
+- #2415597 - Make batching of SQL sources optional, and force map_joinable FALSE.
+
+Migrate 2.7 Release Candidate 1
+===============================
+
+Features and enhancements
+- #2296911 - Add a source handler for IBM DB2.
+- #2256761 - Add a destination handler for variables.
+- #2047815 - Support multi-column source keys in idlist.
+- #1751438 - Add spreadsheet source plugin.
+
+Bug fixes
+- #2403593 - SQL batching messes up cases with altered queries, such as idlist.
+- #2298969 - Verify wizard validation function exists.
+- #2268863 - Fix drush --all option.
+- #2410523 - Remove inconsistent escaping of migrate_drush_path.
+
+Migrate 2.6
+===========
+
+IMPORTANT CHANGES SINCE MIGRATE 2.5
+-----------------------------------
+Migration developers will need to add the "advanced migration information"
+permission to their roles to continue seeing all the info in the UI they're
+used to.
+
+Auto-registration (having classes be registered just based on their class name,
+with no call to registerMigration or definition in hook_migrate_api()) is no
+longer supported. Registration of classes defined in hook_migrate_api() is no
+longer automatic - do a drush migrate-register or use the Register button in the
+UI to register them.
+
+Migration class constructors should now always accept an $arguments array as
+the first parameter and pass it to its parent. This version does support legacy
+migrations which pass a group object, or nothing, but these methods are
+deprecated.
+
+Features and enhancements
+- #2390229 - Make literal strings monolithic.
+- #2391789 - Add extender getter to better support extending wizards.
+- #2363015 - Add support for modifying wizards defined by other modules.
+- #2353527 - Add getter/setter for trackLastImported.
+- #2302929 - Explicitly count IDs for JSON source counts.
+- #2296187 - Batch MigrateSQLSource queries.
+- #2260211 - Allow skipping of file contents in MigrateListFiles.
+- #2259089 - Display actual query for SQL sources.
+- #2224297 - Destination handler for custom blocks.
+- #2370677 - Add removeStep() method to Wizard API.
+- #2306953 - Give basic users a little more information.
+- #2261357 - Add prepareCallback/completeCallback to table destination.
+- #2306923 - Propagate message statuses to drupal_set_message in batch.
+- #2301679 - Display all source key fields in the message view.
+- #2312075 - Add --ignore-highwater option to drush imports
+- #1961316 - Get data from all XML namespaces
+- #1298724 - Add a node revision destination plugin.
+- #1890610 - Add a MongoDB source plugin
+- #2065295 - Add ability to disable other module's hooks during migration.
+- #2051547 - Add ability to revert UI-defined field mappings.
+- #1998632 - Extend MigrateItemsXML to handle an array of XML files.
+- #1990612 - Add a row status argument to MigrateException, allowing rows to
+ be cleanly skipped by throwing exceptions.
+- #2017835 - Add MigrateFileUriAsIs file class, and make file migrations more
+ flexible.
+- #2004426 - UI support for editing dependencies; enable setting dependencies
+ through arguments.
+- #1826112 - A new API has been added to support external modules in developing
+ easy-to-use wizard-based UIs.
+- #1833380 - Major refactoring of the UI, breaking groups and migrations
+ (tasks) into separate pages, introducing an advanced permission and
+ presenting a simplified UI to those with only the basic permission.
+- #1860450 - The UI now has the capability of spawning import/rollback operations
+ in drush, with email notifications of completion. The notification
+ ability is available when running drush at the command line, which
+ may be useful for running imports via cron.
+- #1996602 - A default field handler is now provided that should handle many if
+ not most field types without custom handlers.
+- #1901980 - Support encrypting particular arguments saved to the database, to
+ support wizard implementations that may be prompting for secure
+ database credentials.
+- #1896096 - Add ability to define field mappings via arguments at registration
+ time, and use those to override mappings in code.
+- #1961620 - When editing field mappings, allow destination or source fields to
+ be explicitly set DNM.
+- #1996280 - Allow all field/subfields/options to be edited, and indent
+ subfields/options to make the relationship more clear.
+- #1996350 - Allow sourceMigration to be edited.
+- #1996736 - Add support for hook_migrate_api_alter().
+- #1984568 - Add setters to enable constructing migrations according to a
+ dependency injection pattern.
+- #1835822 - Hash source rows to detect changes.
+- #1975180 - Explicitly check dependency existence so missing dependencies aren't
+ reported as circular.
+- #1984534 - Updating/cleanup of examples to reflect current best practices.
+- #1972600 - Add getter for the MigrateXMLReader member of MigrateSourceXML.
+- #1856694 - Allow disabling of uri encoding.
+- #1835142 - Add names-only option to drush ms command.
+- #1892296 - Increase time limit for batch UI migrations.
+- #1839644 - Groups have been extended to record titles and arguments in the db.
+- #1896920 - Remove auto-registration, and make static registration expicit.
+- #1928956 - Clean up registration/instantiation chicken-and-egg problems, by
+ supporting consistent constructor arguments.
+- #1550878 - Added UI for deregistering migrations, including one-button
+ deregistration of orphans.
+- #1792894 - Remove empty field values, permitting proper defaulting.
+- #1903236 - Allow reset of migrate_migrations() cache.
+- #1894344 - Support warn_on_override for bulk unmigrated methods.
+- #1886404 - Allow subfields to be mapped before primary fields.
+
+Bug fixes
+- #2391891 - idlist with XML source causes exception.
+- #2314077 - Don't merge group dependencies into migration dependencies.
+- #2392683 - Set vocabulary name (bundle) explicitly for term reference fields.
+- #2266395 - Check for valid picture file saving users.
+- #2225551 - Add field validation to entity destination plugins.
+- #2285966 - Added exception handling around constructors.
+- #2370877 - Fix reloading of wizard steps after first.
+- #2358767 - Topological sort to help avoid false circular dependency errors.
+- #2019193 - Properly handle destination DNM status on field mapping save.
+- #2250081 - Fix PostgreSQL failure updating node statistics on comments.
+- #2157933 - Fix PostgreSQL error with empty source migrations.
+- #2177313 - Make sure reused file entities are saved to pick up field changes.
+- #2352971 - Treat empty default values as NULL in field mapping editor.
+- #2347205 - Explicitly default the language for terms.
+- #2307599 - Notice for non-advanced users on rollback.
+- #2305105 - Remove pedantically-deprecated hyphens.
+- #2325237 - Handle language arrays for file fields.
+- #2258909 - Use migrated languages for file fields.
+- #2323605 - Handle single-parent terms properly.
+- #2313495 - Handle multilingual path aliases.
+- #2261227 - Fixed missing subfields on edit mapping form.
+- #2154385 - Add file/line context to source plugin exceptions.
+- #2190255 - Remove duplicate warnings on missing XML source.
+- #2129609 - Remove warnings on MS SQL counts.
+- #2109931 - Pass drush global options to subprocess.
+- #2236279 - Prevent ignore_case from causing created terms to be lower-cased.
+- #2104149 - Sanity-check decrypted arguments.
+- #2047523 - Consistently document destination handler fields() implementations.
+- #2244759 - Rollback migrations in proper (reverse) order in UI.
+- #2184641 - Make saveHighwater() work with PostgreSQL.
+- #2076035 - Properly check key values in saveMessage().
+- #2076035 - Validate that term names are not empty.
+- #2225809 - Use proper API for registering wizard classes.
+- #2105037 - Expose nid/uid destinations when is_new is on.
+- #2145213 - Add SQLMap constructor documentation.
+- #2213033 - Add batch callback documentation.
+- #2095829 - Remove all variables on uninstall.
+- #2072721 - Proper title for migrate_example_baseball feature.
+- #1999228 - Removed AJAX from field mapping form for performance.
+- #2237755 - Fix activeUrl handling in JSON source.
+- #2237891 - Use drupal_register_shutdown_function().
+- #2141409 - Handle drush spawning when Drupal in subdirectory.
+- #2191335 - Remove unused columns from CSV source row.
+- #2210533 - Properly pass directory by reference.
+- #2141569 - Explicitly create group in baseball example.
+- #2159291 - Add message for missing argument to drush migrate-messages.
+- #2019193 - Fix unsetting of DNM source fields through the UI.
+- #2227061 - Fix to subfields in a multi-value context.
+- #2170177 - Properly handle multi-value fields when a subfield is mapped first.
+- #2120205 - Fix bug when upgrading lastimported from D6 to D7.
+- #2109821 - Handle multi-value subfields in the default field handler.
+- #2039649 - Smarter setting of file 'type' field.
+- #2106925 - Fix machine_name generation for legacy migrations.
+- #2102087 - Don't pass --group to drush subprocess.
+- #2099585 - Enable group option on drush migrate-stop.
+- #2088255 - Eliminate notices on migrate_map_hash with track_changes.
+- #2014849 - Make sure (again) statistics properly default to 0.
+- #2042535 - Properly set file_usage for user pictures.
+- #2060631 - Fix notices on poll vote import.
+- #2049689 - Empty values for node is_new caused SQL errors.
+- #2042399 - Node import did not respect revision flag.
+- #2034885 - For XML sources, don't override values populated in prepareRow().
+- #2041267 - Handle lack of potential dependencies on edit page.
+- #2040101 - Breadcrumbs in migration UI missing sections
+- #2033947 - Empty migration display names when matching group names.
+- #2037265 - Static migrations were not registered on module enable.
+- #2021457 - Coding standard improvements for xml.inc.
+- #2030559 - Add deprecation messages for arguments().
+- #1854382 - Fix handling of terms with leading spaces.
+- #2025137 - Ignore --update in the presence of --idlist.
+- #2023813 - Apply defaultValue() for empty XML values.
+- #2023657 - Prevent duplicate aliases when updating nodes.
+- #2021973 - Drush deregistration needs to handle mixed-case machine names.
+- #2017443 - Properly hash XML source rows with track_changes on.
+- #2020095 - Preserve arguments in legacy constructors.
+- #2018841 - Default field handler needs to account for empty column
+ descriptions.
+- #2016173 - Fix fatal error editing MigrationBase migrations.
+- #2015327 - Remove broken automatic field mapping feature.
+- #2014849 - Make sure statistics properly default to 0.
+- #2011024 - Wildcard groups/tasks in the menu hierarchy, saving the need to
+ rebuild menus when migrations are registered/deregistered.
+- #2010884 - Clean up empty default groups on dbupdate.
+- #2004296 - Enforce map/message table names with prefixes.
+- #2009222 - Properly update map/message tables on a non-default connection.
+- #2006158 - Fix warnings on default field handler fields().
+- #1999918 - Move permissions to core Migrate module.
+- #1999290 - Check that an XMLMigration has actually populated $row->xml.
+- #1989012 - Support preserve_files for MigrateFileFid.
+- #1982564 - Properly handle messages for ignored rows.
+- #1989022 - Strip HTML tags from drush fields output.
+- #1988008 - Restore group dependency support.
+- #1985750 - Add missing prepareRollback/completeRollback for file destinations.
+- #1978702 - Check highwater marks against the starting highwater.
+- #1974216 - Change field mapping table PK to a simple serial column.
+- #1977578 - Bad exception handling in MigrateDestinationTableCopy.
+- #1973030 - Handle zero-valued timestamps properly.
+- #1973092 - Fix preservation of sourceMigration when editing mappings.
+- #1968358 - Prevent unnecessary reimport when using track_changes.
+- #1968014 - Fix menu notices on task list page.
+- #1967946 - Missing check on existence of mapping.
+- #1849350 - Make sure encoding of uris doesn't break query strings.
+- #1844316 - Ensure rollback_action is added to all map tables.
+- #1913462 - Fix update functions returning arrays.
+- #1831940 - Allow empty string source keys in handleSourceMigration().
+- #1896042 - Fix incorrect usage of originalQuery.
+- #1954936 - Allow overriding source count in isComplete.
+- #1952430 - Prevent bogus "no error provided" messages.
+- #1931168 - Properly cache class info in _migrate_class_list().
+- #1900914 - Disable email with no custom mail system configured.
+- #1901648 - Missing bundle property on file migrations.
+- #1885362 - Make access callback explicit, to avoid conflict with admin_views.
+- #1880512 - Strip tags from descriptions on drush migrate-mappings.
+- #1872446 - Properly handle updates on role migration.
+- #1871764 - Pass zero values through handleSourceMigration.
+- #1839534 - Handle missing chunk separator in MigrateItemFile.
+- #1854382 - Prevent duplicate terms due to leading/trailing spaces.
+- #1794236 - Namespace detail pages within menu system.
+- #1836426 - Proper check on activeUrl for multiple XML files.
Migrate 2.5
===========
@@ -10,12 +242,6 @@ If your migrations are not explicitly registered, you must request auto-registra
with a "drush mar" (drush migrate-auto-register) command, or by clicking the
"Register" button at admin/content/migrate/registration.
-Bug fixes
-- #1827052 - Properly check for bad XML.
-
-Migrate 2.5 Release Candidate 2
-===============================
-
Features and enhancements
- #1824024 - Destination and field handlers may now be registered through
hook_migrate_api(). Automatic registration of all migration and
@@ -26,14 +252,6 @@ Features and enhancements
hook_migrate_api().
- #1819730 - Make migration of files in a field context non-fatal.
- #1816652 - Provide useful warning when file subfield arrays don't line up.
-
-Bug fixes
-- #1824118 - Make --force work for rollbacks.
-
-Migrate 2.5 Release Candidate 1
-===============================
-
-Features and enhancements
- #1778952 - Enable registration of dynamic migrations via hook_migrate_api().
Add explicit auto-registration of non-dynamic migrations, remove
performing registration on cache clear.
@@ -54,6 +272,8 @@ Features and enhancements
- #1621906 - Support DESTINATION system-of-records for menu links.
Bug fixes
+- #1827052 - Properly check for bad XML.
+- #1824118 - Make --force work for rollbacks.
- #1659150 - Change 'ok' message types to 'status'.
- #1690080 - Deal with self-references in handleSourceMigration().
- #1797644 - Remove bogus assignment on term update.
diff --git a/sites/all/modules/contrib/migrate/migrate/README.txt b/sites/all/modules/contrib/migrate/migrate/README.txt
index 7650c084..d8b290c1 100644
--- a/sites/all/modules/contrib/migrate/migrate/README.txt
+++ b/sites/all/modules/contrib/migrate/migrate/README.txt
@@ -6,8 +6,8 @@ users are included. Plugins permit migration of other types of content.
Usage
-----
-Documentation is at http://drupal.org/node/415260. To get started, enable the
-migrate_example module and browse to admin/content/migrate to see its dashboard.
+Documentation is at http://drupal.org/migrate. To get started, enable the
+migrate_example module and browse to admin/content/migrate to see its dashboard.
The code for this migration is in migrate_example/beer.inc (advanced examples are
in wine.inc). Mimic that file in order to specify your own migrations.
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/base.inc b/sites/all/modules/contrib/migrate/migrate/includes/base.inc
index 05a0fbb3..2369fbd1 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/base.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/base.inc
@@ -37,9 +37,9 @@ abstract class MigrationBase {
}
/**
- * The name of a migration group, used to collect related migrations.
+ * A migration group object, used to collect related migrations.
*
- * @var string
+ * @var MigrateGroup
*/
protected $group;
public function getGroup() {
@@ -55,6 +55,9 @@ abstract class MigrationBase {
public function getDescription() {
return $this->description;
}
+ public function setDescription($description) {
+ $this->description = $description;
+ }
/**
* Save options passed to current operation
@@ -143,9 +146,21 @@ abstract class MigrationBase {
public function getHardDependencies() {
return $this->dependencies;
}
+ public function setHardDependencies(array $dependencies) {
+ $this->dependencies = $dependencies;
+ }
+ public function addHardDependencies(array $dependencies) {
+ $this->dependencies = array_merge($this->dependencies, $dependencies);
+ }
public function getSoftDependencies() {
return $this->softDependencies;
}
+ public function setSoftDependencies(array $dependencies) {
+ $this->softDependencies = $dependencies;
+ }
+ public function addSoftDependencies(array $dependencies) {
+ $this->softDependencies = array_merge($this->softDependencies, $dependencies);
+ }
public function getDependencies() {
return array_merge($this->dependencies, $this->softDependencies);
}
@@ -161,6 +176,13 @@ abstract class MigrationBase {
self::$displayFunction = $display_function;
}
+ /**
+ * Track whether or not we've already displayed an encryption warning
+ *
+ * @var bool
+ */
+ protected static $showEncryptionWarning = TRUE;
+
/**
* The fraction of the memory limit at which an operation will be interrupted.
* Can be overridden by a Migration subclass if one would like to push the
@@ -193,6 +215,14 @@ abstract class MigrationBase {
*/
protected $timeLimit;
+ /**
+ * A time limit in seconds appropriate to be used in a batch
+ * import. Defaults to 240.
+ *
+ * @var int
+ */
+ protected $batchTimeLimit = 240;
+
/**
* MigrateTeamMember objects representing people involved with this
* migration.
@@ -203,6 +233,9 @@ abstract class MigrationBase {
public function getTeam() {
return $this->team;
}
+ public function setTeam(array $team) {
+ $this->team = $team;
+ }
/**
* If provided, an URL for an issue tracking system containing :id where
@@ -214,6 +247,9 @@ abstract class MigrationBase {
public function getIssuePattern() {
return $this->issuePattern;
}
+ public function setIssuePattern($issue_pattern) {
+ $this->issuePattern = $issue_pattern;
+ }
/**
* If we set an error handler (during import), remember the previous one so
@@ -223,6 +259,22 @@ abstract class MigrationBase {
*/
protected $previousErrorHandler = NULL;
+ /**
+ * Arguments configuring a migration.
+ *
+ * @var array
+ */
+ protected $arguments = array();
+ public function getArguments() {
+ return $this->arguments;
+ }
+ public function setArguments(array $arguments) {
+ $this->arguments = $arguments;
+ }
+ public function addArguments(array $arguments) {
+ $this->arguments = array_merge($this->arguments, $arguments);
+ }
+
/**
* Disabling a migration prevents it from running with --all, or individually
* without --force
@@ -233,6 +285,29 @@ abstract class MigrationBase {
public function getEnabled() {
return $this->enabled;
}
+ public function setEnabled($enabled) {
+ $this->enabled = $enabled;
+ }
+
+ /**
+ * Any module hooks which should be disabled during migration processes.
+ *
+ * @var array
+ * Key: Hook name (e.g., 'node_insert')
+ * Value: Array of modules for which to disable this hook (e.g., array('pathauto')).
+ */
+ protected $disableHooks = array();
+ public function getDisableHooks() {
+ return $this->disableHooks;
+ }
+
+ /**
+ * Have we already warned about obsolete constructor argumentss on this request?
+ *
+ * @var bool
+ */
+ static protected $groupArgumentWarning = FALSE;
+ static protected $emptyArgumentsWarning = FALSE;
/**
* Codes representing the result of a rollback or import process.
@@ -284,17 +359,51 @@ abstract class MigrationBase {
}
/**
- * General initialization of a MigrationBase object.
+ * Construction of a MigrationBase instance.
+ *
+ * @param array $arguments
*/
- public function __construct($group = NULL) {
- $this->machineName = $this->generateMachineName();
-
- if (empty($group)) {
- $this->group = MigrateGroup::getInstance('default');
+ public function __construct($arguments = array()) {
+ // Support for legacy code passing a group object as the first parameter.
+ if (is_object($arguments) && is_a($arguments, 'MigrateGroup')) {
+ $this->group = $arguments;
+ $this->arguments['group_name'] = $arguments->getName();
+ if (!self::$groupArgumentWarning &&
+ variable_get('migrate_deprecation_warnings', 1)) {
+ self::displayMessage(t('Passing a group object to a migration constructor is now deprecated - pass through the arguments array passed to the leaf class instead.'));
+ self::$groupArgumentWarning = TRUE;
+ }
}
else {
- $this->group = $group;
+ if (empty($arguments)) {
+ $this->arguments = array();
+ if (!self::$emptyArgumentsWarning &&
+ variable_get('migrate_deprecation_warnings', 1)) {
+ self::displayMessage(t('Passing an empty first parameter to a migration constructor is now deprecated - pass through the arguments array passed to the leaf class instead.'));
+ self::$emptyArgumentsWarning = TRUE;
+ }
+ }
+ else {
+ $this->arguments = $arguments;
+ }
+ if (empty($this->arguments['group_name'])) {
+ $this->arguments['group_name'] = 'default';
+ }
+ $this->group = MigrateGroup::getInstance($this->arguments['group_name']);
}
+ if (isset($this->arguments['machine_name'])) {
+ $this->machineName = $this->arguments['machine_name'];
+ }
+ else {
+ // Deprecated - this supports old code which does not pass the arguments
+ // array through to the base constructor. Remove in the next version.
+ $this->machineName = $this->machineFromClass(get_class($this));
+ }
+ // Make any group arguments directly accessible to the specific migration,
+ // other than group dependencies.
+ $group_arguments = $this->group->getArguments();
+ unset($group_arguments['dependencies']);
+ $this->arguments += $group_arguments;
// Record the memory limit in bytes
$limit = trim(ini_get('memory_limit'));
@@ -330,9 +439,18 @@ abstract class MigrationBase {
$conf['mail_system'][$system] = 'MigrateMailIgnore';
}
}
+ else {
+ $conf['mail_system']['default-system'] = 'MigrateMailIgnore';
+ }
// Make sure we clear our semaphores in case of abrupt exit
- register_shutdown_function(array($this, 'endProcess'));
+ drupal_register_shutdown_function(array($this, 'endProcess'));
+
+ // Save any hook disablement information.
+ if (isset($this->arguments['disable_hooks']) &&
+ is_array($this->arguments['disable_hooks'])) {
+ $this->disableHooks = $this->arguments['disable_hooks'];
+ }
}
/**
@@ -358,7 +476,10 @@ abstract class MigrationBase {
* @param string $machine_name
* @param array $arguments
*/
- static public function registerMigration($class_name, $machine_name = NULL, array $arguments = array()) {
+ static public function registerMigration($class_name, $machine_name = NULL,
+ array $arguments = array()) {
+ // Support for legacy migration code - in later releases, the machine_name
+ // should always be explicit.
if (!$machine_name) {
$machine_name = self::machineFromClass($class_name);
}
@@ -368,18 +489,28 @@ abstract class MigrationBase {
array('!name' => $machine_name)));
}
- // Making sure the machine name is in the arguments array helps with
- // chicken-and-egg problems in determining the machine name.
- if (!isset($arguments['machine_name'])) {
- $arguments['machine_name'] = $machine_name;
+ // We no longer have any need to store the machine_name in the arguments.
+ if (isset($arguments['machine_name'])) {
+ unset($arguments['machine_name']);
}
+ if (isset($arguments['group_name'])) {
+ $group_name = $arguments['group_name'];
+ unset($arguments['group_name']);
+ }
+ else {
+ $group_name = 'default';
+ }
+
+ $arguments = self::encryptArguments($arguments);
+
// Register the migration if it's not already there; if it is,
// update the class and arguments in case they've changed.
db_merge('migrate_status')
->key(array('machine_name' => $machine_name))
->fields(array(
'class_name' => $class_name,
+ 'group_name' => $group_name,
'arguments' => serialize($arguments)
))
->execute();
@@ -395,17 +526,27 @@ abstract class MigrationBase {
$rows_deleted = db_delete('migrate_status')
->condition('machine_name', $machine_name)
->execute();
+ // Make sure the group gets deleted if we were the only member.
+ MigrateGroup::deleteOrphans();
}
/**
- * By default, the migration machine name is the class name (with the
- * Migration suffix, if present, stripped).
+ * The migration machine name is stored in the arguments.
+ *
+ * @return string
*/
protected function generateMachineName() {
- $class_name = get_class($this);
- return self::machineFromClass($class_name);
+ return $this->arguments['machine_name'];
}
+ /**
+ * Given only a class name, derive a machine name (the class name with the
+ * "Migration" suffix, if any, removed).
+ *
+ * @param $class_name
+ *
+ * @return string
+ */
protected static function machineFromClass($class_name) {
if (preg_match('/Migration$/', $class_name)) {
$machine_name = drupal_substr($class_name, 0,
@@ -422,38 +563,74 @@ abstract class MigrationBase {
*
* @param string $machine_name
*/
+
+ /**
+ * Return the single instance of the given migration.
+ *
+ * @param $machine_name
+ * The unique machine name of the migration to retrieve.
+ * @param string $class_name
+ * Deprecated - no longer used, class name is retrieved from migrate_status.
+ * @param array $arguments
+ * Deprecated - no longer used, arguments are retrieved from migrate_status.
+ *
+ * @return MigrationBase
+ */
static public function getInstance($machine_name, $class_name = NULL, array $arguments = array()) {
$migrations = &drupal_static(__FUNCTION__, array());
// Otherwise might miss cache hit on case difference
$machine_name_key = drupal_strtolower($machine_name);
if (!isset($migrations[$machine_name_key])) {
- // Skip the query if our caller already made it
- if (!$class_name) {
- // See if we know about this migration
- $row = db_select('migrate_status', 'ms')
- ->fields('ms', array('class_name', 'arguments'))
- ->condition('machine_name', $machine_name)
- ->execute()
- ->fetchObject();
- if ($row) {
- $class_name = $row->class_name;
- $arguments = unserialize($row->arguments);
+ // See if we know about this migration
+ $row = db_select('migrate_status', 'ms')
+ ->fields('ms', array('class_name', 'group_name', 'arguments'))
+ ->condition('machine_name', $machine_name)
+ ->execute()
+ ->fetchObject();
+ if ($row) {
+ $class_name = $row->class_name;
+ $arguments = unserialize($row->arguments);
+ $arguments = self::decryptArguments($arguments);
+ $arguments['group_name'] = $row->group_name;
+ }
+ else {
+ // Can't find a migration with this name
+ self::displayMessage(t('No migration found with machine name !machine',
+ array('!machine' => $machine_name)));
+ return NULL;
+ }
+ $arguments['machine_name'] = $machine_name;
+ if (class_exists($class_name)) {
+ try {
+ $migrations[$machine_name_key] = new $class_name($arguments);
}
- else {
- // Can't find a migration with this name
- throw new MigrateException(t('No migration found with machine name !machine',
+ catch (Exception $e) {
+ self::displayMessage(t('Migration !machine could not be constructed.',
array('!machine' => $machine_name)));
+ self::displayMessage($e->getMessage());
+ return NULL;
}
}
- $migrations[$machine_name_key] = new $class_name($arguments);
+ else {
+ self::displayMessage(t('No migration class !class found',
+ array('!class' => $class_name)));
+ return NULL;
+ }
+ if (isset($arguments['dependencies'])) {
+ $migrations[$machine_name_key]->setHardDependencies(
+ $arguments['dependencies']);
+ }
+ if (isset($arguments['soft_dependencies'])) {
+ $migrations[$machine_name_key]->setSoftDependencies(
+ $arguments['soft_dependencies']);
+ }
}
return $migrations[$machine_name_key];
}
/**
- * Identifies whether this migration is "dynamic" (that is, allows multiple
- * instances distinguished by differing parameters). A dynamic class should
- * override this with a return value of TRUE.
+ * @deprecated - No longer a useful distinction between "status" and "dynamic"
+ * migrations.
*/
static public function isDynamic() {
return FALSE;
@@ -625,9 +802,16 @@ abstract class MigrationBase {
if (!empty($this->highwaterField['type']) && $this->highwaterField['type'] == 'int') {
// If the highwater is an integer type, we need to force the DB server
// to treat the varchar highwater field as an integer (otherwise it will
- // think '5' > '10'). CAST(highwater AS INTEGER) would be ideal, but won't
- // work in MySQL. This hack is thought to be portable.
- $query->where('(highwater+0) < :highwater', array(':highwater' => $highwater));
+ // think '5' > '10').
+ switch (Database::getConnection()->databaseType()) {
+ case 'pgsql':
+ $query->where('(CASE WHEN highwater=\'\' THEN 0 ELSE CAST(highwater AS INTEGER) END) < :highwater', array(':highwater' => intval($highwater)));
+ break;
+ default:
+ // CAST(highwater AS INTEGER) would be ideal, but won't
+ // work in MySQL. This hack is thought to be portable.
+ $query->where('(highwater+0) < :highwater', array(':highwater' => $highwater));
+ }
}
else {
$query->condition('highwater', $highwater, '<');
@@ -684,7 +868,7 @@ abstract class MigrationBase {
else {
foreach ($this->dependencies as $dependency) {
$migration = MigrationBase::getInstance($dependency);
- if (!$migration->isComplete()) {
+ if (!$migration || !$migration->isComplete()) {
return FALSE;
}
}
@@ -699,7 +883,7 @@ abstract class MigrationBase {
$incomplete = array();
foreach ($this->getDependencies() as $dependency) {
$migration = MigrationBase::getInstance($dependency);
- if (!$migration->isComplete()) {
+ if (!$migration || !$migration->isComplete()) {
$incomplete[] = $dependency;
}
}
@@ -752,6 +936,17 @@ abstract class MigrationBase {
))
->execute();
}
+
+ // If we're disabling any hooks, reset the static module_implements cache so
+ // it is rebuilt with the specified hooks removed by our
+ // hook_module_implements_alter(). By setting #write_cache to FALSE, we
+ // ensure that our munged version of the hooks array does not get written
+ // to the persistent cache and interfere with other Drupal processes.
+ if (!empty($this->disableHooks)) {
+ $implementations = &drupal_static('module_implements');
+ $implementations = array();
+ $implementations['#write_cache'] = FALSE;
+ }
}
/**
@@ -904,6 +1099,14 @@ abstract class MigrationBase {
return $return;
}
+ /**
+ * Set the PHP time limit. This method may be called from batch callbacks
+ * before calling the processImport method.
+ */
+ public function setBatchTimeLimit() {
+ drupal_set_time_limit($this->batchTimeLimit);
+ }
+
/**
* A derived migration class does the actual rollback or import work in these
* methods - we cannot declare them abstract because some classes may define
@@ -999,6 +1202,132 @@ abstract class MigrationBase {
}
}
+ /**
+ * Encrypt an incoming value. Detects for existence of the Drupal 'Encrypt'
+ * module or the mcrypt PHP extension.
+ *
+ * @param string $value
+ * @return string The encrypted value.
+ */
+ static public function encrypt($value) {
+ if (module_exists('encrypt')) {
+ $value = encrypt($value);
+ }
+ else if (extension_loaded('mcrypt')) {
+ // Mimic encrypt module to ensure compatibility
+ $key = drupal_substr(variable_get('drupal_private_key', 'no_key'), 0, 32);
+ $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
+ $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
+ $value = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $value,
+ MCRYPT_MODE_ECB, $iv);
+
+ $encryption_array['text'] = $value;
+ // For forward compatibility with the encrypt module.
+ $encryption_array['method'] = 'mcrypt_rij_256';
+ $encryption_array['key_name'] = 'drupal_private_key';
+ $value = serialize($encryption_array);
+ }
+ else {
+ if (self::$showEncryptionWarning) {
+ MigrationBase::displayMessage(t('Encryption of secure migration information is not supported. Ensure the Encrypt module or mcrypt PHP extension is installed for this functionality.',
+ array(
+ '@encrypt' => 'http://drupal.org/project/encrypt',
+ '@mcrypt' => 'http://php.net/manual/en/book.mcrypt.php',
+ )
+ ),
+ 'warning');
+ self::$showEncryptionWarning = FALSE;
+ }
+ }
+ return $value;
+ }
+
+ /**
+ * Decrypt an incoming value.
+ *
+ * @param string $value
+ * @return string The encrypted value
+ */
+ static public function decrypt($value) {
+ if (module_exists('encrypt')) {
+ $value = decrypt($value);
+ }
+ else if (extension_loaded('mcrypt')) {
+ // Mimic encrypt module to ensure compatibility
+ $encryption_array = unserialize($value);
+ $method = $encryption_array['method']; // Not used right now
+ $text = $encryption_array['text'];
+ $key_name = $encryption_array['key_name']; // Not used right now
+
+ $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
+ $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
+ $key = drupal_substr(variable_get('drupal_private_key', 'no_key'), 0, 32);
+ $value = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $text,
+ MCRYPT_MODE_ECB, $iv);
+ }
+ else {
+ if (self::$showEncryptionWarning) {
+ MigrationBase::displayMessage(t('Encryption of secure migration information is not supported. Ensure the Encrypt module or mcrypt PHP extension is installed for this functionality.',
+ array(
+ '@encrypt' => 'http://drupal.org/project/encrypt',
+ '@mcrypt' => 'http://php.net/manual/en/book.mcrypt.php',
+ )
+ ),
+ 'warning');
+ self::$showEncryptionWarning = FALSE;
+ }
+ }
+ return $value;
+ }
+
+ /**
+ * Make sure any arguments we want to be encrypted get encrypted.
+ *
+ * @param array $arguments
+ *
+ * @return array
+ */
+ static public function encryptArguments(array $arguments) {
+ if (isset($arguments['encrypted_arguments'])) {
+ foreach ($arguments['encrypted_arguments'] as $argument_name) {
+ if (isset($arguments[$argument_name])) {
+ $arguments[$argument_name] = self::encrypt(
+ serialize($arguments[$argument_name]));
+ }
+ }
+ }
+ return $arguments;
+ }
+
+ /**
+ * Make sure any arguments we want to be decrypted get decrypted.
+ *
+ * @param array $arguments
+ *
+ * @return array
+ */
+ static public function decryptArguments(array $arguments) {
+ if (isset($arguments['encrypted_arguments'])) {
+ foreach ($arguments['encrypted_arguments'] as $argument_name) {
+ if (isset($arguments[$argument_name])) {
+ $decrypted_string = self::decrypt($arguments[$argument_name]);
+ // A decryption failure will return FALSE and issue a notice. We need
+ // to distinguish a failure from a serialized FALSE.
+ $unserialized_value = @unserialize($decrypted_string);
+ if ($unserialized_value === FALSE && $decrypted_string != serialize(FALSE)) {
+ self::displayMessage(t('Failed to decrypt argument %argument_name',
+ array('%argument_name' => $argument_name)));
+ unset($arguments[$argument_name]);
+ }
+ else {
+ $arguments[$argument_name] = $unserialized_value;
+ }
+ }
+ }
+ }
+ return $arguments;
+ }
+
/**
* Convert an incoming string (which may be a UNIX timestamp, or an arbitrarily-formatted
* date/time string) to a UNIX timestamp.
@@ -1006,16 +1335,16 @@ abstract class MigrationBase {
* @param string $value
*/
static public function timestamp($value) {
- // Default empty values to now
- if (empty($value)) {
- return time();
- }
-
// Does it look like it's already a timestamp? Just return it
if (is_numeric($value)) {
return $value;
}
+ // Default empty values to now
+ if (empty($value)) {
+ return time();
+ }
+
$date = new DateTime($value);
$time = $date->format('U');
if ($time == FALSE) {
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/destination.inc b/sites/all/modules/contrib/migrate/migrate/includes/destination.inc
index 15c499d8..1323862d 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/destination.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/destination.inc
@@ -105,11 +105,22 @@ abstract class MigrateDestination {
* All destination handlers should be derived from MigrateDestinationHandler
*/
abstract class MigrateDestinationHandler extends MigrateHandler {
- // abstract function arguments(...)
+ // Any one or more of these methods may be implemented
+
/**
- * Any one or more of these methods may be implemented
+ * Documentation of any fields added to the destination by this handler.
+ *
+ * @param $entity_type
+ * The entity type (node, user, etc.) for which to list fields.
+ * @param $bundle
+ * The bundle (article, blog, etc.), if any, for which to list fields.
+ * @param Migration $migration
+ * Optionally, the migration providing the context.
+ * @return array
+ * An array keyed by field name, with field descriptions as values.
*/
- //abstract public function fields();
+ //abstract public function fields($entity_type, $bundle, $migration = NULL);
+
//abstract public function prepare($entity, stdClass $row);
//abstract public function complete($entity, stdClass $row);
}
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/exception.inc b/sites/all/modules/contrib/migrate/migrate/includes/exception.inc
index 2bd6f99c..518b7879 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/exception.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/exception.inc
@@ -6,13 +6,30 @@
*/
class MigrateException extends Exception {
+ /**
+ * The level of the error being reported (a Migration::MESSAGE_* constant)
+ * @var int
+ */
protected $level;
public function getLevel() {
return $this->level;
}
- public function __construct($message, $level = Migration::MESSAGE_ERROR) {
+ /**
+ * The status to record in the map table for the current item (a
+ * MigrateMap::STATUS_* constant)
+ *
+ * @var int
+ */
+ protected $status;
+ public function getStatus() {
+ return $this->status;
+ }
+
+ public function __construct($message, $level = Migration::MESSAGE_ERROR,
+ $status = MigrateMap::STATUS_FAILED) {
$this->level = $level;
+ $this->status = $status;
parent::__construct($message);
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/field_mapping.inc b/sites/all/modules/contrib/migrate/migrate/includes/field_mapping.inc
index 3b016bda..403d0f44 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/field_mapping.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/field_mapping.inc
@@ -29,6 +29,19 @@ class MigrateFieldMapping {
return $this->sourceField;
}
+ /**
+ * @var int
+ */
+ const MAPPING_SOURCE_CODE = 1;
+ const MAPPING_SOURCE_DB = 2;
+ protected $mappingSource = self::MAPPING_SOURCE_CODE;
+ public function getMappingSource() {
+ return $this->mappingSource;
+ }
+ public function setMappingSource($mapping_source) {
+ $this->mappingSource = $mapping_source;
+ }
+
/**
* Default value for simple mappings, when there is no source mapping or the
* source field is empty. If both this and the sourceField are omitted, the
@@ -96,6 +109,7 @@ class MigrateFieldMapping {
* 'source_field' - Name of the source field in the incoming row containing the
* value to be assigned
* 'default_value' - A constant value to be assigned in the absence of source_field
+ * Deprecated - subfield notation is now preferred.
*
* @var array
*/
@@ -173,6 +187,9 @@ class MigrateFieldMapping {
}
public function arguments($arguments) {
+ if (variable_get('migrate_deprecation_warnings', 1)) {
+ MigrationBase::displayMessage(t('The field mapping arguments() method is now deprecated - please use subfield notation instead.'));
+ }
$this->arguments = $arguments;
return $this;
}
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/group.inc b/sites/all/modules/contrib/migrate/migrate/includes/group.inc
index 920419b2..bdfe0a16 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/group.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/group.inc
@@ -7,7 +7,7 @@
class MigrateGroup {
/**
- * The name of the group - used to identify it in drush commands.
+ * The machine name of the group - used to identify it in drush commands.
*
* @var string
*/
@@ -16,6 +16,27 @@ class MigrateGroup {
return $this->name;
}
+ /**
+ * The user-visible title of the group.
+ *
+ * @var string
+ */
+ protected $title;
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Domain-specific arguments for the group (to be applied to all migrations
+ * in the group).
+ *
+ * @var array
+ */
+ protected $arguments = array();
+ public function getArguments() {
+ return $this->arguments;
+ }
+
/**
* List of groups this group is dependent on.
*
@@ -35,9 +56,11 @@ class MigrateGroup {
static public function groups() {
$groups = array();
$dependent_groups = array();
+ $dependencies_list = array();
$required_groups = array();
foreach (self::$groupList as $name => $group) {
$dependencies = $group->getDependencies();
+ $dependencies_list[$name] = $dependencies;
if (count($dependencies) > 0) {
// Set groups with dependencies aside for reordering
$dependent_groups[$name] = $group;
@@ -48,29 +71,11 @@ class MigrateGroup {
$groups[$name] = $group;
}
}
- $iterations = 0;
- while (count($dependent_groups) > 0) {
- if ($iterations++ > 20) {
- $group_names = implode(',', array_keys($dependent_groups));
- throw new MigrateException(t('Failure to sort migration list - most likely due ' .
- 'to circular dependencies involving groups !group_names',
- array('!group_names' => $group_names)));
- }
- foreach ($dependent_groups as $name => $group) {
- $ready = TRUE;
- // Scan all the dependencies for this group and make sure they're all
- // in the final list
- foreach ($group->getDependencies() as $dependency) {
- if (!isset($groups[$dependency])) {
- $ready = FALSE;
- break;
- }
- }
- if ($ready) {
- // Yes they are! Move this group to the final list
- $groups[$name] = $group;
- unset($dependent_groups[$name]);
- }
+
+ $ordered_groups = migrate_order_dependencies($dependencies_list);
+ foreach ($ordered_groups as $name) {
+ if (!isset($groups[$name])) {
+ $groups[$name] = $dependent_groups[$name];
}
}
@@ -86,8 +91,10 @@ class MigrateGroup {
* @param array $dependencies
* List of dependent groups.
*/
- public function __construct($name, $dependencies = array()) {
+ public function __construct($name, $dependencies = array(), $title = '', $arguments = array()) {
$this->name = $name;
+ $this->title = $title;
+ $this->arguments = $arguments;
$this->dependencies = $dependencies;
}
@@ -102,8 +109,94 @@ class MigrateGroup {
*/
static public function getInstance($name, $dependencies = array()) {
if (empty(self::$groupList[$name])) {
- self::$groupList[$name] = new MigrateGroup($name, $dependencies);
+ $row = db_select('migrate_group', 'mg')
+ ->fields('mg')
+ ->condition('name', $name)
+ ->execute()
+ ->fetchObject();
+ if ($row) {
+ $arguments = unserialize($row->arguments);
+ $arguments = MigrationBase::decryptArguments($arguments);
+ if (empty($dependencies) && isset($arguments['dependencies'])) {
+ $dependencies = $arguments['dependencies'];
+ }
+ self::$groupList[$name] = new MigrateGroup($name, $dependencies,
+ $row->title, $arguments);
+ }
+ else {
+ self::register($name);
+ self::$groupList[$name] = new MigrateGroup($name, $dependencies);
+ }
}
return self::$groupList[$name];
}
+
+ /**
+ * Register a new migration group in the migrate_group table.
+ *
+ * @param string $name
+ * The machine name (unique identifier) for the group.
+ *
+ * @param string $title
+ * A user-visible title for the group. Defaults to the machine name.
+ *
+ * @param array $arguments
+ * An array of group arguments - generally data that applies to all migrations
+ * in the group.
+ */
+ static public function register($name, $title = NULL, array $arguments = array()) {
+ if (!$title) {
+ $title = $name;
+ }
+
+ $arguments = MigrationBase::encryptArguments($arguments);
+
+ // Register the migration if it's not already there; if it is,
+ // update the class and arguments in case they've changed.
+ db_merge('migrate_group')
+ ->key(array('name' => $name))
+ ->fields(array(
+ 'title' => $title,
+ 'arguments' => serialize($arguments)
+ ))
+ ->execute();
+ }
+
+
+ /**
+ * Deregister a migration group - remove it from the database, and also
+ * remove migrations attached to it.
+ *
+ * @param string $name
+ * (Machine) name of the group to deregister.
+ */
+ static public function deregister($name) {
+ $result = db_select('migrate_status', 'ms')
+ ->fields('ms', array('machine_name'))
+ ->condition('group_name', $name)
+ ->execute();
+ foreach ($result as $row) {
+ Migration::deregisterMigration($row->machine_name);
+ }
+
+ db_delete('migrate_group')
+ ->condition('name', $name)
+ ->execute();
+ }
+
+ /**
+ * Remove any groups which no longer contain any migrations.
+ */
+ static public function deleteOrphans() {
+ $query = db_select('migrate_group', 'mg');
+ $query->addField('mg', 'name');
+ $query->leftJoin('migrate_status', 'ms', 'mg.name=ms.group_name');
+ $query->isNull('ms.machine_name');
+ $result = $query->execute();
+ foreach ($result as $row) {
+ db_delete('migrate_group')
+ ->condition('name', $row->name)
+ ->execute();
+ }
+ }
}
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/map.inc b/sites/all/modules/contrib/migrate/migrate/includes/map.inc
index ed76db50..e32d54fd 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/map.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/map.inc
@@ -43,12 +43,27 @@ abstract class MigrateMap implements Iterator {
*/
protected $sourceKeyMap, $destinationKeyMap;
+ /**
+ * Get the source key map.
+ */
+ public function getSourceKeyMap() {
+ return $this->sourceKeyMap;
+ }
+
/**
* Boolean determining whether to track last_imported times in map tables
*
* @var boolean
*/
protected $trackLastImported = FALSE;
+ public function getTrackLastImported() {
+ return $this->trackLastImported;
+ }
+ public function setTrackLastImported($trackLastImported) {
+ if (is_bool($trackLastImported)) {
+ $this->trackLastImported = $trackLastImported;
+ }
+ }
/**
* Save a mapping from the key values in the source row to the destination
@@ -58,9 +73,11 @@ abstract class MigrateMap implements Iterator {
* @param $dest_ids
* @param $status
* @param $rollback_action
+ * @param $hash
*/
abstract public function saveIDMapping(stdClass $source_row, array $dest_ids,
- $status = MigrateMap::STATUS_IMPORTED, $rollback_action = MigrateMap::ROLLBACK_DELETE);
+ $status = MigrateMap::STATUS_IMPORTED,
+ $rollback_action = MigrateMap::ROLLBACK_DELETE, $hash = NULL);
/**
* Record a message related to a source record
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/migration.inc b/sites/all/modules/contrib/migrate/migrate/includes/migration.inc
index 55ce3877..b4b7f29a 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/migration.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/migration.inc
@@ -22,6 +22,9 @@ abstract class Migration extends MigrationBase {
public function getSource() {
return $this->source;
}
+ public function setSource(MigrateSource $source) {
+ $this->source = $source;
+ }
/**
* Destination object for the migration, derived from MigrateDestination.
@@ -32,6 +35,9 @@ abstract class Migration extends MigrationBase {
public function getDestination() {
return $this->destination;
}
+ public function setDestination(MigrateDestination $destination) {
+ $this->destination = $destination;
+ }
/**
* Map object tracking relationships between source and destination data
@@ -42,6 +48,9 @@ abstract class Migration extends MigrationBase {
public function getMap() {
return $this->map;
}
+ public function setMap(MigrateMap $map) {
+ $this->map = $map;
+ }
/**
* Indicate whether the primary system of record for this migration is the
@@ -59,6 +68,9 @@ abstract class Migration extends MigrationBase {
public function getSystemOfRecord() {
return $this->systemOfRecord;
}
+ public function setSystemOfRecord($system_of_record) {
+ $this->systemOfRecord = $system_of_record;
+ }
/**
* Specify value of needs_update for current map row. Usually set by
@@ -75,6 +87,12 @@ abstract class Migration extends MigrationBase {
* @var int
*/
protected $defaultRollbackAction = MigrateMap::ROLLBACK_DELETE;
+ public function getDefaultRollbackAction() {
+ return $this->defaultRollbackAction;
+ }
+ public function setDefaultRollbackAction($rollback_action) {
+ $this->defaultRollbackAction = $rollback_action;
+ }
/**
* The rollback action to be saved for the current row.
@@ -84,13 +102,62 @@ abstract class Migration extends MigrationBase {
public $rollbackAction;
/**
- * Simple mappings between destination fields (keys) and source fields (values).
+ * Field mappings defined in code.
*
* @var array
*/
- protected $fieldMappings = array();
+ protected $storedFieldMappings = array();
+ protected $storedFieldMappingsRetrieved = FALSE;
+ public function getStoredFieldMappings() {
+ if (!$this->storedFieldMappingsRetrieved) {
+ $this->loadFieldMappings();
+ $this->storedFieldMappingsRetrieved = TRUE;
+ }
+ return $this->storedFieldMappings;
+ }
+
+ /**
+ * Field mappings retrieved from storage.
+ *
+ * @var array
+ */
+ protected $codedFieldMappings = array();
+ public function getCodedFieldMappings() {
+ return $this->codedFieldMappings;
+ }
+
+ /**
+ * All field mappings, with those retrieved from the database overriding those
+ * defined in code.
+ *
+ * @var array
+ */
+ protected $allFieldMappings = array();
public function getFieldMappings() {
- return $this->fieldMappings;
+ if (empty($allFieldMappings)) {
+ $this->allFieldMappings = array_merge($this->getCodedFieldMappings(),
+ $this->getStoredFieldMappings());
+ // If there are multiple mappings of a given source field to no
+ // destination field, keep only the last (so the UI can override a source
+ // field DNM that was defined in code).
+ $no_destination = array();
+ foreach ($this->allFieldMappings as $destination_field => $mapping) {
+ // If the source field is not mapped to a destination field, the
+ // array index is integer.
+ if (is_int($destination_field)) {
+ $source_field = $mapping->getSourceField();
+ if (isset($no_destination[$source_field])) {
+ unset($this->allFieldMappings[$no_destination[$source_field]]);
+ unset($no_destination[$source_field]);
+ }
+ $no_destination[$source_field] = $destination_field;
+ }
+ }
+
+ // Make sure primary fields come before their subfields
+ ksort($this->allFieldMappings);
+ }
+ return $this->allFieldMappings;
}
/**
@@ -119,6 +186,9 @@ abstract class Migration extends MigrationBase {
public function getHighwaterField() {
return $this->highwaterField;
}
+ public function setHighwaterField(array $highwater_field) {
+ $this->highwaterField = $highwater_field;
+ }
/**
* The object currently being constructed
@@ -143,8 +213,29 @@ abstract class Migration extends MigrationBase {
/**
* General initialization of a Migration object.
*/
- public function __construct($group = NULL) {
- parent::__construct($group);
+ public function __construct($arguments = array()) {
+ parent::__construct($arguments);
+ }
+
+ /**
+ * Register a new migration process in the migrate_status table. This will
+ * generally be used in two contexts - by the class detection code for
+ * static (one instance per class) migrations, and by the module implementing
+ * dynamic (parameterized class) migrations.
+ *
+ * @param string $class_name
+ * @param string $machine_name
+ * @param array $arguments
+ */
+ static public function registerMigration($class_name, $machine_name = NULL,
+ array $arguments = array()) {
+ // Record any field mappings provided via arguments.
+ if (isset($arguments['field_mappings'])) {
+ self::saveFieldMappings($machine_name, $arguments['field_mappings']);
+ unset($arguments['field_mappings']);
+ }
+
+ parent::registerMigration($class_name, $machine_name, $arguments);
}
/**
@@ -161,9 +252,16 @@ abstract class Migration extends MigrationBase {
try {
// Remove map and message tables
$migration = self::getInstance($machine_name);
- $migration->map->destroy();
+ if ($migration && method_exists($migration, 'getMap')) {
+ $migration->getMap()->destroy();
+ }
- // TODO: Clear log entries? Or keep for historical purposes?
+ // @todo: Clear log entries? Or keep for historical purposes?
+
+ // Remove stored field mappings for this migration
+ $rows_deleted = db_delete('migrate_field_mapping')
+ ->condition('machine_name', $machine_name)
+ ->execute();
// Call the parent deregistration (which clears migrate_status) last, the
// above will reference it.
@@ -174,6 +272,51 @@ abstract class Migration extends MigrationBase {
}
}
+ /**
+ * Record an array of field mappings to the database.
+ *
+ * @param $machine_name
+ * @param array $field_mappings
+ */
+ static public function saveFieldMappings($machine_name, array $field_mappings) {
+ // Clear existing field mappings
+ db_delete('migrate_field_mapping')
+ ->condition('machine_name', $machine_name)
+ ->execute();
+ foreach ($field_mappings as $field_mapping) {
+ $destination_field = $field_mapping->getDestinationField();
+ $source_field = $field_mapping->getSourceField();
+ db_insert('migrate_field_mapping')
+ ->fields(array(
+ 'machine_name' => $machine_name,
+ 'destination_field' => is_null($destination_field) ? '' : $destination_field,
+ 'source_field' => is_null($source_field) ? '' : $source_field,
+ 'options' => serialize($field_mapping)
+ ))
+ ->execute();
+ }
+ }
+
+ /**
+ * Load any stored field mappings from the database.
+ */
+ public function loadFieldMappings() {
+ $result = db_select('migrate_field_mapping', 'mfm')
+ ->fields('mfm', array('destination_field', 'source_field', 'options'))
+ ->condition('machine_name', $this->machineName)
+ ->execute();
+ foreach ($result as $row) {
+ $field_mapping = unserialize($row->options);
+ $field_mapping->setMappingSource(MigrateFieldMapping::MAPPING_SOURCE_DB);
+ if (empty($row->destination_field)) {
+ $this->storedFieldMappings[] = $field_mapping;
+ }
+ else {
+ $this->storedFieldMappings[$row->destination_field] = $field_mapping;
+ }
+ }
+ }
+
////////////////////////////////////////////////////////////////////
// Processing
@@ -193,25 +336,25 @@ abstract class Migration extends MigrationBase {
$warn_on_override = TRUE) {
// Warn of duplicate mappings
if ($warn_on_override && !is_null($destination_field) &&
- isset($this->fieldMappings[$destination_field])) {
+ isset($this->codedFieldMappings[$destination_field])) {
self::displayMessage(
t('!name addFieldMapping: !dest was previously mapped from !source, overridden',
array('!name' => $this->machineName, '!dest' => $destination_field,
- '!source' => $this->fieldMappings[$destination_field]->getSourceField())),
+ '!source' => $this->codedFieldMappings[$destination_field]->getSourceField())),
'warning');
}
$mapping = new MigrateFieldMapping($destination_field, $source_field);
if (is_null($destination_field)) {
- $this->fieldMappings[] = $mapping;
+ $this->codedFieldMappings[] = $mapping;
}
else {
- $this->fieldMappings[$destination_field] = $mapping;
+ $this->codedFieldMappings[$destination_field] = $mapping;
}
return $mapping;
}
/**
- * Remove any existing mappings for a given destination or source field.
+ * Remove any existing coded mappings for a given destination or source field.
*
* @param string $destination_field
* Name of the destination field.
@@ -220,12 +363,12 @@ abstract class Migration extends MigrationBase {
*/
public function removeFieldMapping($destination_field, $source_field = NULL) {
if (isset($destination_field)) {
- unset($this->fieldMappings[$destination_field]);
+ unset($this->codedFieldMappings[$destination_field]);
}
if (isset($source_field)) {
- foreach ($this->fieldMappings as $key => $mapping) {
+ foreach ($this->codedFieldMappings as $key => $mapping) {
if ($mapping->getSourceField() == $source_field) {
- unset($this->fieldMappings[$key]);
+ unset($this->codedFieldMappings[$key]);
}
}
}
@@ -254,12 +397,12 @@ abstract class Migration extends MigrationBase {
* @param string $issue_group
* Issue group name to apply to the generated mappings (defaults to 'DNM').
*/
- public function addUnmigratedDestinations(array $fields, $issue_group = NULL) {
+ public function addUnmigratedDestinations(array $fields, $issue_group = NULL, $warn_on_override = TRUE) {
if (!$issue_group) {
$issue_group = t('DNM');
}
foreach ($fields as $field) {
- $this->addFieldMapping($field)
+ $this->addFieldMapping($field, NULL, $warn_on_override)
->issueGroup($issue_group);
}
}
@@ -274,12 +417,12 @@ abstract class Migration extends MigrationBase {
* @param string $issue_group
* Issue group name to apply to the generated mappings (defaults to 'DNM').
*/
- public function addUnmigratedSources(array $fields, $issue_group = NULL) {
+ public function addUnmigratedSources(array $fields, $issue_group = NULL, $warn_on_override = TRUE) {
if (!$issue_group) {
$issue_group = t('DNM');
}
foreach ($fields as $field) {
- $this->addFieldMapping(NULL, $field)
+ $this->addFieldMapping(NULL, $field, $warn_on_override)
->issueGroup($issue_group);
}
}
@@ -289,7 +432,7 @@ abstract class Migration extends MigrationBase {
* source rows have been processed).
*/
public function isComplete() {
- $total = $this->source->count(TRUE);
+ $total = $this->sourceCount(TRUE);
// If the source is uncountable, we have no way of knowing if it's
// complete, so stipulate that it is.
if ($total < 0) {
@@ -551,8 +694,8 @@ abstract class Migration extends MigrationBase {
}
catch (Exception $e) {
self::displayMessage(
- t('Migration failed with source plugin exception: !e',
- array('!e' => $e->getMessage())));
+ t('Migration failed with source plugin exception: %e, in %file:%line',
+ array('%e' => $e->getMessage(), '%file' => $e->getFile(), '%line' => $e->getLine())));
return MigrationBase::RESULT_FAILED;
}
while ($this->source->valid()) {
@@ -570,28 +713,33 @@ abstract class Migration extends MigrationBase {
$ids = $this->destination->import($this->destinationValues, $this->sourceValues);
migrate_instrument_stop('destination import');
if ($ids) {
- $this->map->saveIDMapping($this->sourceValues, $ids, $this->needsUpdate,
- $this->rollbackAction);
+ $this->map->saveIDMapping($this->sourceValues, $ids,
+ $this->needsUpdate, $this->rollbackAction,
+ $data_row->migrate_map_hash);
$this->successes_since_feedback++;
$this->total_successes++;
}
else {
$this->map->saveIDMapping($this->sourceValues, array(),
- MigrateMap::STATUS_FAILED, $this->rollbackAction);
- $message = t('New object was not saved, no error provided');
- $this->saveMessage($message);
- self::displayMessage($message);
+ MigrateMap::STATUS_FAILED, $this->rollbackAction,
+ $data_row->migrate_map_hash);
+ if ($this->map->messageCount() == 0) {
+ $message = t('New object was not saved, no error provided');
+ $this->saveMessage($message);
+ self::displayMessage($message);
+ }
}
}
catch (MigrateException $e) {
$this->map->saveIDMapping($this->sourceValues, array(),
- MigrateMap::STATUS_FAILED, $this->rollbackAction);
+ $e->getStatus(), $this->rollbackAction, $data_row->migrate_map_hash);
$this->saveMessage($e->getMessage(), $e->getLevel());
self::displayMessage($e->getMessage());
}
catch (Exception $e) {
$this->map->saveIDMapping($this->sourceValues, array(),
- MigrateMap::STATUS_FAILED, $this->rollbackAction);
+ MigrateMap::STATUS_FAILED, $this->rollbackAction,
+ $data_row->migrate_map_hash);
$this->handleException($e);
}
$this->total_processed++;
@@ -624,8 +772,8 @@ abstract class Migration extends MigrationBase {
}
catch (Exception $e) {
self::displayMessage(
- t('Migration failed with source plugin exception: !e',
- array('!e' => $e->getMessage())));
+ t('Migration failed with source plugin exception: %e, in %file:%line',
+ array('%e' => $e->getMessage(), '%file' => $e->getFile(), '%line' => $e->getLine())));
return MigrationBase::RESULT_FAILED;
}
}
@@ -679,7 +827,7 @@ abstract class Migration extends MigrationBase {
$row = $this->source->current();
// Cheat for XML migrations, which don't pick up the source values
// until applyMappings() applies the xpath()
- if (is_a($this, 'XMLMigration')) {
+ if (is_a($this, 'XMLMigration') && isset($row->xml)) {
$this->sourceValues = $row;
$this->applyMappings();
$row = $this->sourceValues;
@@ -1038,7 +1186,7 @@ abstract class Migration extends MigrationBase {
*/
protected function applyMappings() {
$this->destinationValues = new stdClass;
- foreach ($this->fieldMappings as $mapping) {
+ foreach ($this->getFieldMappings() as $mapping) {
$destination = $mapping->getDestinationField();
// Skip mappings with no destination (source fields marked DNM)
if ($destination) {
@@ -1055,7 +1203,7 @@ abstract class Migration extends MigrationBase {
// If there's a source mapping, and a source value in the data row, copy
// to the destination
- if ($source && property_exists($this->sourceValues, $source)) {
+ if ($source && isset($this->sourceValues->{$source})) {
$destination_values = $this->sourceValues->$source;
}
// Otherwise, apply the default value (if any)
@@ -1141,7 +1289,8 @@ abstract class Migration extends MigrationBase {
}
// We've seen a subfield, so add as an array value.
else {
- $this->destinationValues->{$destination_field}[] = $destination_values;
+ $this->destinationValues->{$destination_field} = array_merge(
+ (array)$destination_values, $this->destinationValues->{$destination_field});
}
}
}
@@ -1202,15 +1351,17 @@ abstract class Migration extends MigrationBase {
$results = array();
// Each $source_key will be an array of key values
foreach ($source_keys as $source_key) {
- // If any source keys are empty, skip this set
+ // If any source keys are NULL, skip this set
$continue = FALSE;
foreach ($source_key as $value) {
- if (empty($value) && $value !== 0 && $value !== '0') {
+ if (!isset($value)) {
$continue = TRUE;
break;
}
}
- if ($continue || empty($source_key)) {
+ // Occasionally $source_key comes through with an empty string.
+ $sanity_check = array_filter($source_key);
+ if ($continue || empty($source_key) || empty($sanity_check)) {
continue;
}
// Loop through each source migration, checking for an existing dest ID.
@@ -1261,7 +1412,7 @@ abstract class Migration extends MigrationBase {
}
else {
$value = reset($results);
- return empty($value) ? NULL : $value;
+ return empty($value) && $value !== 0 && $value !== '0' ? NULL : $value;
}
}
@@ -1372,7 +1523,7 @@ abstract class Migration extends MigrationBase {
/**
* Save any messages we've queued up to the message table.
*/
- protected function saveQueuedMessages() {
+ public function saveQueuedMessages() {
foreach ($this->queuedMessages as $queued_message) {
$this->saveMessage($queued_message['message'], $queued_message['level']);
}
@@ -1391,12 +1542,19 @@ abstract class Migration extends MigrationBase {
}
/**
- * Convenience class - deriving from this rather than directory from Migration
- * ensures that a class will not be registered as a migration itself - it is
- * the implementor's responsibility to register each instance of a dynamic
- * migration class.
+ * @deprecated - This class is no longer necessary, inherit directly from
+ * Migration instead.
*/
abstract class DynamicMigration extends Migration {
+ static $deprecationWarning = FALSE;
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ if (variable_get('migrate_deprecation_warnings', 1) &&
+ !self::$deprecationWarning) {
+ self::displayMessage(t('The DynamicMigration class is no longer necessary and is now deprecated - please derive your migration classes directly from Migration.'));
+ self::$deprecationWarning = TRUE;
+ }
+ }
/**
* Overrides default of FALSE
*/
diff --git a/sites/all/modules/contrib/migrate/migrate/includes/source.inc b/sites/all/modules/contrib/migrate/migrate/includes/source.inc
index 14fca25a..39396a41 100644
--- a/sites/all/modules/contrib/migrate/migrate/includes/source.inc
+++ b/sites/all/modules/contrib/migrate/migrate/includes/source.inc
@@ -78,6 +78,20 @@ abstract class MigrateSource implements Iterator {
*/
protected $highwaterField;
+ /**
+ * The highwater mark at the beginning of the import operation.
+ *
+ * @var
+ */
+ protected $originalHighwater = '';
+
+ /**
+ * Used in the case of multiple key sources that need to use idlist.
+ *
+ * @var string
+ */
+ protected $multikeySeparator = ':';
+
/**
* List of source IDs to process.
*
@@ -116,6 +130,14 @@ abstract class MigrateSource implements Iterator {
*/
protected $skipCount = FALSE;
+ /**
+ * If TRUE, we will maintain hashed source rows to determine whether incoming
+ * data has changed.
+ *
+ * @var bool
+ */
+ protected $trackChanges = FALSE;
+
/**
* By default, next() will directly read the map row and add it to the data
* row. A source plugin implementation may do this itself (in particular, the
@@ -186,6 +208,9 @@ abstract class MigrateSource implements Iterator {
if (!empty($options['cache_key'])) {
$this->cacheKey = $options['cache_key'];
}
+ if (!empty($options['track_changes'])) {
+ $this->trackChanges = $options['track_changes'];
+ }
}
/**
@@ -229,6 +254,9 @@ abstract class MigrateSource implements Iterator {
$this->numProcessed = 0;
$this->numIgnored = 0;
$this->highwaterField = $this->activeMigration->getHighwaterField();
+ if (!empty($this->highwaterField)) {
+ $this->originalHighwater = $this->activeMigration->getHighwater();
+ }
if ($this->activeMigration->getOption('idlist')) {
$this->idList = explode(',', $this->activeMigration->getOption('idlist'));
}
@@ -248,9 +276,11 @@ abstract class MigrateSource implements Iterator {
public function next() {
$this->currentKey = NULL;
$this->currentRow = NULL;
+
migrate_instrument_start(get_class($this) . ' getNextRow');
while ($row = $this->getNextRow()) {
migrate_instrument_stop(get_class($this) . ' getNextRow');
+
// Populate the source key for this row
$this->currentKey = $this->activeMigration->prepareKey(
$this->activeMap->getSourceKey(), $row);
@@ -267,23 +297,22 @@ abstract class MigrateSource implements Iterator {
}
}
- // First, determine if this row should be passed to prepareRow(), or skipped
- // entirely. The rules are:
+ // First, determine if this row should be passed to prepareRow(), or
+ // skipped entirely. The rules are:
// 1. If there's an explicit idlist, that's all we care about (ignore
// highwaters and map rows).
$prepared = FALSE;
if (!empty($this->idList)) {
- if (in_array(reset($this->currentKey), $this->idList)) {
- // In the list, fall through.
- }
- else {
- // Not in the list, skip it
- $this->currentRow = NULL;
- continue;
+ if (!in_array(reset($this->currentKey), $this->idList)) {
+ $compoundKey = implode($this->multikeySeparator, $this->currentKey);
+ if (count($this->currentKey) > 1 && !in_array($compoundKey, $this->idList)) {
+ // Could not find the key, skip.
+ continue;
+ }
}
}
- // 2. If the row is not in the map (we have never tried to import it before),
- // we always want to try it.
+ // 2. If the row is not in the map (we have never tried to import it
+ // before), we always want to try it.
elseif (!isset($row->migrate_map_sourceid1)) {
// Fall through
}
@@ -293,36 +322,52 @@ abstract class MigrateSource implements Iterator {
}
// 4. At this point, we have a row which has previously been imported and
// not marked for update. If we're not using highwater marks, then we
- // will not take this row.
+ // will not take this row. Except, if we're looking for changes in the
+ // data, we need to go through prepareRow() before we can decide to
+ // skip it.
elseif (empty($this->highwaterField)) {
- // No highwater, skip
- $this->currentRow = NULL;
- continue;
+ if ($this->trackChanges) {
+ if ($this->prepareRow($row) !== FALSE) {
+ if ($this->dataChanged($row)) {
+ // This is a keeper
+ $this->currentRow = $row;
+ break;
+ }
+ else {
+ // No change, skip it.
+ continue;
+ }
+ }
+ else {
+ // prepareRow() told us to skip it.
+ continue;
+ }
+ }
+ else {
+ // No highwater and not tracking changes, skip.
+ continue;
+ }
}
// 5. The initial highwater mark, before anything is migrated, is ''. We
// want to make sure we don't mistakenly skip rows with a highwater
// field value of 0, so explicitly handle '' here.
- elseif ($this->activeMigration->getHighwater() === '') {
+ elseif ($this->originalHighwater === '') {
// Fall through
}
- // 6. So, we are using highwater marks. Take the row if its highwater field
- // value is greater than the saved mark, otherwise skip it.
+ // 6. So, we are using highwater marks. Take the row if its highwater
+ // field value is greater than the saved mark, otherwise skip it.
else {
// Call prepareRow() here, in case the highwaterField needs preparation
if ($this->prepareRow($row) !== FALSE) {
- if ($row->{$this->highwaterField['name']} > $this->activeMigration->getHighwater()) {
+ if ($row->{$this->highwaterField['name']} > $this->originalHighwater) {
$this->currentRow = $row;
break;
}
else {
// Skip
- $this->currentRow = NULL;
continue;
}
}
- else {
- $this->currentRow = NULL;
- }
$prepared = TRUE;
}
@@ -358,6 +403,10 @@ abstract class MigrateSource implements Iterator {
migrate_instrument_stop(get_class($this->activeMigration) . ' prepareRow');
// We're explicitly skipping this row - keep track in the map table
if ($return === FALSE) {
+ // Make sure we replace any previous messages for this item with any
+ // new ones.
+ $this->activeMigration->getMap()->delete($this->currentKey, TRUE);
+ $this->activeMigration->saveQueuedMessages();
$this->activeMigration->getMap()->saveIDMapping($row, array(),
MigrateMap::STATUS_IGNORED, $this->activeMigration->rollbackAction);
$this->numIgnored++;
@@ -366,8 +415,62 @@ abstract class MigrateSource implements Iterator {
}
else {
$return = TRUE;
+ // When tracking changed data, We want to quietly skip (rather than
+ // "ignore") rows with changes. The caller needs to make that decision,
+ // so we need to provide them with the necessary information (before and
+ // after hashes).
+ if ($this->trackChanges) {
+ $unhashed_row = clone ($row);
+ // Remove all map data, otherwise we'll have a false positive on the
+ // second import (attempt) on a row.
+ foreach ($unhashed_row as $field => $data) {
+ if (strpos($field, 'migrate_map_') === 0) {
+ unset($unhashed_row->$field);
+ }
+ }
+ $row->migrate_map_original_hash = isset($row->migrate_map_hash) ?
+ $row->migrate_map_hash : '';
+ $row->migrate_map_hash = $this->hash($unhashed_row);
+ }
+ else {
+ $row->migrate_map_hash = '';
+ }
}
+
$this->numProcessed++;
return $return;
}
+
+ /**
+ * Determine whether this row has changed, and therefore whether it should
+ * be processed.
+ *
+ * @param $row
+ *
+ * @return bool
+ */
+ protected function dataChanged($row) {
+ if ($row->migrate_map_original_hash != $row->migrate_map_hash) {
+ $return = TRUE;
+ }
+ else {
+ $return = FALSE;
+ }
+
+ return $return;
+ }
+
+ /**
+ * Generate a hash of the source row.
+ *
+ * @param $row
+ *
+ * @return string
+ */
+ protected function hash($row) {
+ migrate_instrument_start('MigrateSource::hash');
+ $hash = md5(serialize($row));
+ migrate_instrument_stop('MigrateSource::hash');
+ return $hash;
+ }
}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate.api.php b/sites/all/modules/contrib/migrate/migrate/migrate.api.php
index 3afa2425..8b4190f0 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate.api.php
+++ b/sites/all/modules/contrib/migrate/migrate/migrate.api.php
@@ -6,17 +6,96 @@
*/
/**
- * Registers your module as an implementor of Migrate-based classes.
+ * Registers your module as an implementor of Migrate-based classes and provides
+ * default configuration for migration processes.
+ *
+ * @return
+ * An associative array with the following keys (of which only 'api' is
+ * required):
+ * - api: Always 2 for any module implementing the Migrate 2 API.
+ * - groups: An associative array, keyed by group machine name, defining one
+ * or more migration groups. Each value is an associative array - the 'title'
+ * key defines a user-visible name for the group; any other values are
+ * passed as arguments to all migrations in the group.
+ * - migrations: An associative array, keyed by migration machine name,
+ * defining one or more migrations. Each value is an associative array - any
+ * keys other than the following are passed as arguments to the migration
+ * constructor:
+ * - class_name (required): The name of the class implementing the migration.
+ * - group_name: The machine name of the group containing the migration.
+ * - disable_hooks: An associative array, keyed by hook name, listing hook
+ * implementations to be disabled during migration. Each value is an
+ * array of module names whose implementations of the hook in the key is
+ * to be disabled.
+ * - destination handlers: An array of classes implementing destination
+ * handlers.
+ * - field handlers: An array of classes implementing field handlers.
+ * - wizard classes: An array of classes that provide Migrate UI wizards.
+ * - wizard extenders: An array of classes that extend Migrate UI wizards.
+ * Keys are the wizard classes, values are arrays of extender classes.
+ *
+ * See system_hook_info() for all hook groups defined by Drupal core.
+ *
+ * @see hook_migrate_api_alter().
*/
function hook_migrate_api() {
$api = array(
'api' => 2,
+ 'groups' => array(
+ 'legacy' => array(
+ 'title' => t('Import from legacy system'),
+ // Default format for all content migrations
+ 'default_format' => 'filtered_html',
+ ),
+ ),
+ 'migrations' => array(
+ 'ExampleUser' => array(
+ 'class_name' => 'ExampleUserMigration',
+ 'group_name' => 'legacy',
+ 'default_role' => 'member', // Added to constructor $arguments
+ ),
+ 'ExampleNode' => array(
+ 'class_name' => 'ExampleNodeMigration',
+ 'group_name' => 'legacy',
+ 'default_uid' => 1, // Added to constructor $arguments
+ 'disable_hooks' => array(
+ // Improve migration performance, and prevent accidental emails.
+ 'node_insert' => array(
+ 'expensive_module',
+ 'email_notification_module',
+ ),
+ 'node_update' => array(
+ 'expensive_module',
+ 'email_notification_module',
+ ),
+ ),
+ ),
+ ),
);
return $api;
}
+/**
+ * Alter information from all implementations of hook_migrate_api().
+ *
+ * @param array $info
+ * An array of results from hook_migrate_api(), keyed by module name.
+ *
+ * @see hook_migrate_api().
+ */
+function hook_migrate_api_alter(array &$info) {
+ // Override the class for another module's migration - say, to add some
+ // additional preprocessing in prepareRow().
+ if (isset($info['MODULE_NAME']['migrations']['ExampleNode'])) {
+ $info['MODULE_NAME']['migrations']['ExampleNode']['class_name'] = 'MyBetterExampleNodeMigration';
+ }
+}
+
/**
* Provides text to be displayed at the top of the dashboard page (migrate_ui).
+ *
+ * @return
+ * Translated text for display on the dashboard page.
*/
function hook_migrate_overview() {
return t('Listed below are all the migration processes defined for migration
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate.drush.inc b/sites/all/modules/contrib/migrate/migrate/migrate.drush.inc
index e788c683..d5fcd07c 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate.drush.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate.drush.inc
@@ -17,12 +17,14 @@ function migrate_drush_command() {
'instrument' => 'Capture performance information (timer, memory, or all)',
'force' => 'Force an operation to run, even if all dependencies are not satisfied',
'group' => 'Name of the migration group to run',
+ 'notify' => 'Send email notification upon completion of operation',
);
$items['migrate-status'] = array(
'description' => 'List all migrations with current status.',
'options' => array(
'refresh' => 'Recognize new migrations and update counts',
'group' => 'Name of the migration group to list',
+ 'names-only' => 'Only return names, not all the details (faster)',
),
'arguments' => array(
'migration' => 'Restrict to a single migration. Optional',
@@ -129,8 +131,6 @@ function migrate_drush_command() {
$items['migrate-rollback'] = array(
'description' => 'Roll back the destination objects from a given migration',
'options' => $migration_options,
- // We will bootstrap to login from within the command callback.
- 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL,
'arguments' => array(
'migration' => 'Name of migration(s) to roll back. Delimit multiple using commas.',
),
@@ -144,12 +144,13 @@ function migrate_drush_command() {
'drupal dependencies' => array('migrate'),
'aliases' => array('mr'),
);
- $migration_options['update'] = 'In addition to processing unimported items from the source, update previously-imported items with new data';
+ $migration_options['update'] = 'In addition to processing unprocessed items from the source, update previously-imported items with new data';
$migration_options['needs-update'] =
'Reimport up to 10K records where needs_update=1. This option is only needed when your Drupal DB is on a different DB server from your source data. Otherwise, these records get migrated with just migrate-import.';
$migration_options['stop'] = 'Stop specified migration(s) if applicable.';
$migration_options['rollback'] = 'Rollback specified migration(s) if applicable.';
$migration_options['file_function'] = 'Override file function to use when migrating images.';
+ $migration_options['ignore-highwater'] = 'Ignore the highwater field during migration';
$items['migrate-import'] = array(
'description' => 'Perform one or more migration processes',
'options' => $migration_options,
@@ -172,7 +173,8 @@ function migrate_drush_command() {
);
$items['migrate-stop'] = array(
'description' => 'Stop an active migration operation',
- 'options' => array('all' => 'Stop all active migration operations'),
+ 'options' => array('all' => 'Stop all active migration operations',
+ 'group' => 'Name of a specific migration group to stop'),
'arguments' => array(
'migration' => 'Name of migration to stop',
),
@@ -198,21 +200,30 @@ function migrate_drush_command() {
);
$items['migrate-deregister'] = array(
'description' => 'Remove all tracking of a migration',
- 'options' => array('orphans' => 'Remove tracking for any migrations whose implementing class no longer exists'),
+ 'options' => array(
+ 'orphans' => 'Remove tracking for any migrations whose implementing class no longer exists',
+ 'group' => 'Remove tracking of a migration group, and any migrations assigned to it',
+ ),
'arguments' => array(
'migration' => 'Name of migration to deregister',
),
'examples' => array(
'migrate-deregister Article' => 'Deregister the Article migration',
'migrate-deregister --orphans' => 'Deregister any no-longer-implemented migrations',
+ 'migrate-deregister --group=myblog' => 'Deregister the myblog group and all migrations within it',
),
'drupal dependencies' => array('migrate'),
);
$items['migrate-auto-register'] = array(
- 'description' => 'Register any newly-defined migration classes',
+ 'description' => 'Register any newly defined migration classes',
'drupal dependencies' => array('migrate'),
'aliases' => array('mar'),
);
+ $items['migrate-register'] = array(
+ 'description' => 'Register or reregister any statically defined migrations',
+ 'drupal dependencies' => array('migrate'),
+ 'aliases' => array('mreg'),
+ );
$items['migrate-wipe'] = array(
'description' => 'Delete all nodes from specified content types.',
'examples' => array(
@@ -236,9 +247,11 @@ function migrate_drush_command() {
*/
function drush_migrate_get_options() {
$options = array();
- $blacklist = array('stop', 'rollback', 'update', 'all');
+ $blacklist = array('stop', 'rollback', 'update', 'all', 'group');
$command = drush_parse_command();
- foreach ($command['options'] as $key => $value) {
+ $global_options = drush_get_global_options();
+ $opts = array_merge($command['options'], $global_options);
+ foreach ($opts as $key => $value) {
// Strip leading --
$key = ltrim($key, '-');
if (!in_array($key, $blacklist)) {
@@ -281,6 +294,7 @@ function drush_migrate_status($name = NULL) {
try {
$refresh = drush_get_option('refresh');
$group_option = drupal_strtolower(drush_get_option('group'));
+ $names_only = drush_get_option('names-only');
// Validate input and load Migration(s).
if ($name) {
@@ -311,56 +325,69 @@ function drush_migrate_status($name = NULL) {
if ($group_members_count == 1) {
// An empty line and the headers.
$table[] = array('');
- $table[] = array(dt('Group: !name', array('!name' => $group->getName())), dt('Total'), dt('Imported'), dt('Unimported'), dt('Status'), dt('Last imported'));
+ if ($names_only) {
+ $table[] = array(dt('Group: !name',
+ array('!name' => $group->getName())));
+ }
+ else {
+ $table[] = array(dt('Group: !name',
+ array('!name' => $group->getName())), dt('Total'), dt('Imported'),
+ dt('Unprocessed'), dt('Status'), dt('Last imported'));
+ }
}
- $has_counts = TRUE;
- if (method_exists($migration, 'sourceCount')) {
- $total = $migration->sourceCount($refresh);
- if ($total < 0) {
+ if (!$names_only) {
+ $has_counts = TRUE;
+ if (method_exists($migration, 'sourceCount')) {
+ $total = $migration->sourceCount($refresh);
+ if ($total < 0) {
+ $has_counts = FALSE;
+ $total = dt('N/A');
+ }
+ }
+ else {
$has_counts = FALSE;
$total = dt('N/A');
}
+ if (method_exists($migration, 'importedCount')) {
+ $imported = $migration->importedCount();
+ $processed = $migration->processedCount();
+ }
+ else {
+ $has_counts = FALSE;
+ $imported = dt('N/A');
+ }
+ if ($has_counts) {
+ $unimported = $total - $processed;
+ }
+ else {
+ $unimported = dt('N/A');
+ }
+ $status = $migration->getStatus();
+ switch ($status) {
+ case MigrationBase::STATUS_IDLE:
+ $status = dt('Idle');
+ break;
+ case MigrationBase::STATUS_IMPORTING:
+ $status = dt('Importing');
+ break;
+ case MigrationBase::STATUS_ROLLING_BACK:
+ $status = dt('Rolling back');
+ break;
+ case MigrationBase::STATUS_STOPPING:
+ $status = dt('Stopping');
+ break;
+ case MigrationBase::STATUS_DISABLED:
+ $status = dt('Disabled');
+ break;
+ default:
+ $status = dt('Unknown');
+ break;
+ }
+ $table[] = array($migration->getMachineName(), $total, $imported, $unimported, $status, $migration->getLastImported());
}
else {
- $has_counts = FALSE;
- $total = dt('N/A');
+ $table[] = array($migration->getMachineName());
}
- if (method_exists($migration, 'importedCount')) {
- $imported = $migration->importedCount();
- $processed = $migration->processedCount();
- }
- else {
- $has_counts = FALSE;
- $imported = dt('N/A');
- }
- if ($has_counts) {
- $unimported = $total - $processed;
- }
- else {
- $unimported = dt('N/A');
- }
- $status = $migration->getStatus();
- switch ($status) {
- case MigrationBase::STATUS_IDLE:
- $status = dt('Idle');
- break;
- case MigrationBase::STATUS_IMPORTING:
- $status = dt('Importing');
- break;
- case MigrationBase::STATUS_ROLLING_BACK:
- $status = dt('Rolling back');
- break;
- case MigrationBase::STATUS_STOPPING:
- $status = dt('Stopping');
- break;
- case MigrationBase::STATUS_DISABLED:
- $status = dt('Disabled');
- break;
- default:
- $status = dt('Unknown');
- break;
- }
- $table[] = array($migration->getMachineName(), $total, $imported, $unimported, $status, $migration->getLastImported());
}
}
drush_print_table($table);
@@ -382,7 +409,7 @@ function drush_migrate_fields_destination($args = NULL) {
if (method_exists($destination, 'fields')) {
$table = array();
foreach ($destination->fields($migration) as $machine_name => $description) {
- $table[] = array($description, $machine_name);
+ $table[] = array(strip_tags($description), $machine_name);
}
drush_print_table($table);
}
@@ -407,7 +434,7 @@ function drush_migrate_fields_source($args = NULL) {
if (method_exists($source, 'fields')) {
$table = array();
foreach ($source->fields() as $machine_name => $description) {
- $table[] = array($description, $machine_name);
+ $table[] = array(strip_tags($description), $machine_name);
}
drush_print_table($table);
}
@@ -438,14 +465,20 @@ function drush_migrate_mappings($args = NULL) {
$dest_descriptions = array();
if (method_exists($destination, 'fields')) {
foreach ($destination->fields($migration) as $machine_name => $description) {
- $dest_descriptions[$machine_name] = $description;
+ if (is_array($description)) {
+ $description = reset($description);
+ }
+ $dest_descriptions[$machine_name] = strip_tags($description);
}
}
$source = $migration->getSource();
$src_descriptions = array();
if (method_exists($source, 'fields')) {
foreach ($source->fields() as $machine_name => $description) {
- $src_descriptions[$machine_name] = $description;
+ if (is_array($description)) {
+ $description = reset($description);
+ }
+ $src_descriptions[$machine_name] = strip_tags($description);
}
}
}
@@ -545,6 +578,10 @@ function drush_migrate_mappings($args = NULL) {
* Display messages for a migration.
*/
function drush_migrate_messages($migration_name) {
+ if (!trim($migration_name)) {
+ drush_log(dt('You must specify a migration name'), 'status');
+ return;
+ }
try {
$migration = MigrationBase::getInstance($migration_name);
if (is_a($migration, 'Migration')) {
@@ -783,6 +820,21 @@ function drush_migrate_audit($args = NULL) {
*/
function drush_migrate_rollback($args = NULL) {
try {
+ if (drush_get_option('notify', FALSE)) {
+ // Capture non-informational output for mailing
+ ob_start();
+ ob_implicit_flush(FALSE);
+ // Save original mail setup, which Migrate will disable, so we can
+ // restore it later.
+ global $conf;
+ if (!empty($conf['mail_system'])) {
+ $mail_system = $conf['mail_system'];
+ }
+ else {
+ $mail_system = NULL;
+ }
+ }
+
$migrations = drush_migrate_get_migrations($args);
// Rollback in reverse order
@@ -873,6 +925,37 @@ function drush_migrate_rollback($args = NULL) {
if ($_migrate_track_timer && !drush_get_context('DRUSH_DEBUG')) {
drush_print_timers();
}
+
+ // Notify user
+ if (drush_get_option('notify')) {
+ if (is_null($mail_system)) {
+ unset($conf['mail_system']);
+ }
+ else {
+ $conf['mail_system'] = $mail_system;
+ }
+ _drush_migrate_notify();
+ }
+}
+
+/**
+ * Send email notification to the user running the operation.
+ */
+function _drush_migrate_notify() {
+ global $user;
+ if ($user->uid) {
+ $uid = $user->uid;
+ }
+ else {
+ $uid = 1;
+ }
+ $account = user_load($uid);
+ $params['account'] = $account;
+ $params['output'] = ob_get_contents();
+ drush_print_r(ob_get_status());
+ ob_end_flush();
+ drupal_mail('migrate_ui', 'import_complete', $account->mail,
+ user_preferred_language($account), $params);
}
function drush_migrate_get_migrations($args) {
@@ -883,7 +966,7 @@ function drush_migrate_get_migrations($args) {
$seen = $start === TRUE ? TRUE : FALSE;
foreach ($migration_objects as $name => $migration) {
- if (!$seen && (drupal_strtolower($start) . 'migration' == drupal_strtolower($name))) {
+ if (!$seen && (drupal_strtolower($start) == drupal_strtolower($name))) {
// We found our starting migration. $seen is always TRUE now.
$seen = TRUE;
}
@@ -1040,6 +1123,20 @@ function drush_migrate_pre_migrate_import($args = NULL) {
*/
function drush_migrate_import($args = NULL) {
try {
+ if (drush_get_option('notify', FALSE)) {
+ // Capture non-informational output for mailing
+ ob_start();
+ ob_implicit_flush(FALSE);
+ // Save original mail setup, which Migrate will disable, so we can
+ // restore it later.
+ global $conf;
+ if (!empty($conf['mail_system'])) {
+ $mail_system = $conf['mail_system'];
+ }
+ else {
+ $mail_system = NULL;
+ }
+ }
$migrations = drush_migrate_get_migrations($args);
$options = array();
if ($idlist = drush_get_option('idlist', FALSE)) {
@@ -1101,8 +1198,12 @@ function drush_migrate_import($args = NULL) {
foreach ($migrations as $machine_name => $migration) {
drush_log(dt("Importing '!description' migration",
array('!description' => $machine_name)));
- if (drush_get_option('update')) {
+ if (drush_get_option('update') && !$idlist) {
$migration->prepareUpdate();
+
+ if (drush_get_option('ignore-highwater')) {
+ $migration->setHighwaterField(array());
+ }
}
if (drush_get_option('needs-update')) {
$map_rows = $migration->getMap()->getRowsNeedingUpdate(10000);
@@ -1190,14 +1291,25 @@ function drush_migrate_import($args = NULL) {
if ($_migrate_track_timer && !drush_get_context('DRUSH_DEBUG')) {
drush_print_timers();
}
+
+ // Notify user
+ if (drush_get_option('notify')) {
+ if (is_null($mail_system)) {
+ unset($conf['mail_system']);
+ }
+ else {
+ $conf['mail_system'] = $mail_system;
+ }
+ _drush_migrate_notify();
+ }
}
-//**
-// * Stop clearing or importing a given content set.
-// *
-// * @param $content_set
-// * The name of the Migration
-// */
+/**
+ * Stop clearing or importing a given content set.
+ *
+ * @param $content_set
+ * The name of the Migration
+ */
function drush_migrate_stop($args = NULL) {
try {
$migrations = drush_migrate_get_migrations($args);
@@ -1231,31 +1343,40 @@ function drush_migrate_reset_status($args = NULL) {
}
/**
- * Deregister a given migration, or all orphaned migrations. Note that the
- * migration might no longer "exist" (the class implementation might be gone),
- * so we can't count on being able to instantiate it, or use migrate_migrations().
+ * Deregister a given migration, migration group, or all orphaned migrations.
+ * Note that the migration might no longer "exist" (the class implementation
+ * might be gone), so we can't count on being able to instantiate it, or use
+ * migrate_migrations().
*/
function drush_migrate_deregister($args = NULL) {
try {
$orphans = drush_get_option('orphans');
- if ($orphans) {
- $migrations = array();
- $result = db_select('migrate_status', 'ms')
- ->fields('ms', array('class_name', 'machine_name'))
- ->execute();
- foreach ($result as $row) {
- if (!class_exists($row->class_name)) {
- $migrations[] = $row->machine_name;
- }
- }
+ $group = drush_get_option('group');
+ if ($group) {
+ MigrateGroup::deregister($group);
+ drush_log(dt("Deregistered group '!description' and all its migrations",
+ array('!description' => $group)), 'success');
}
else {
- $migrations = explode(',', $args);
- }
- foreach ($migrations as $machine_name) {
- drush_migrate_deregister_migration($machine_name);
- drush_log(dt("Deregistered '!description' migration",
- array('!description' => $machine_name)), 'success');
+ if ($orphans) {
+ $migrations = array();
+ $result = db_select('migrate_status', 'ms')
+ ->fields('ms', array('class_name', 'machine_name'))
+ ->execute();
+ foreach ($result as $row) {
+ if (!class_exists($row->class_name)) {
+ $migrations[] = $row->machine_name;
+ }
+ }
+ }
+ else {
+ $migrations = explode(',', $args);
+ }
+ foreach ($migrations as $machine_name) {
+ drush_migrate_deregister_migration(drupal_strtolower($machine_name));
+ drush_log(dt("Deregistered '!description' migration",
+ array('!description' => $machine_name)), 'success');
+ }
}
}
catch (MigrateException $e) {
@@ -1277,13 +1398,28 @@ function drush_migrate_deregister_migration($machine_name) {
db_delete('migrate_status')
->condition('machine_name', $machine_name)
->execute();
+ db_delete('migrate_field_mapping')
+ ->condition('machine_name', $machine_name)
+ ->execute();
}
/**
- * Register any previously-unrecognized non-dynamic migrations.
+ * Auto-registration is no longer supported. This command should be removed
+ * entirely in a future point release.
+ *
+ * @deprecated
*/
function drush_migrate_auto_register($args = NULL) {
- migrate_autoregister();
+ drush_log(dt('The auto-registration feature has been removed. Migrations '
+ . 'must now be explicitly registered.'), 'error');
+}
+
+/**
+ * Register any migrations defined in hook_migrate_api().
+ */
+function drush_migrate_register($args = NULL) {
+ migrate_static_registration();
+ drush_log(dt('All statically defined migrations have been (re)registered.'), 'success');
}
/**
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate.info b/sites/all/modules/contrib/migrate/migrate/migrate.info
index face2c96..5cab8b60 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate.info
+++ b/sites/all/modules/contrib/migrate/migrate/migrate.info
@@ -1,6 +1,6 @@
name = "Migrate"
description = "Import content from external sources"
-package = "Development"
+package = "Migration"
core = 7.x
files[] = includes/base.inc
@@ -14,6 +14,7 @@ files[] = includes/map.inc
files[] = includes/source.inc
files[] = includes/team.inc
files[] = migrate.mail.inc
+files[] = plugins/destinations/block_custom.inc
files[] = plugins/destinations/entity.inc
files[] = plugins/destinations/term.inc
files[] = plugins/destinations/user.inc
@@ -28,15 +29,19 @@ files[] = plugins/destinations/table_copy.inc
files[] = plugins/destinations/menu.inc
files[] = plugins/destinations/menu_links.inc
files[] = plugins/destinations/statistics.inc
+files[] = plugins/destinations/variable.inc
files[] = plugins/sources/csv.inc
+files[] = plugins/sources/db2.inc
files[] = plugins/sources/files.inc
files[] = plugins/sources/json.inc
files[] = plugins/sources/list.inc
+files[] = plugins/sources/mongodb.inc
files[] = plugins/sources/multiitems.inc
files[] = plugins/sources/sql.inc
files[] = plugins/sources/sqlmap.inc
files[] = plugins/sources/mssql.inc
files[] = plugins/sources/oracle.inc
+files[] = plugins/sources/spreadsheet.inc
files[] = plugins/sources/xml.inc
files[] = tests/import/options.test
files[] = tests/plugins/destinations/comment.test
@@ -46,9 +51,9 @@ files[] = tests/plugins/destinations/term.test
files[] = tests/plugins/destinations/user.test
files[] = tests/plugins/sources/xml.test
-; Information added by drupal.org packaging script on 2012-11-07
-version = "7.x-2.5"
+; Information added by Drupal.org packaging script on 2015-02-09
+version = "7.x-2.7"
core = "7.x"
project = "migrate"
-datestamp = "1352299007"
+datestamp = "1423521491"
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate.install b/sites/all/modules/contrib/migrate/migrate/migrate.install
index 87d469aa..e8d730fa 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate.install
+++ b/sites/all/modules/contrib/migrate/migrate/migrate.install
@@ -9,6 +9,8 @@ function migrate_schema() {
$schema = array();
$schema['migrate_status'] = migrate_schema_status();
$schema['migrate_log'] = migrate_schema_log();
+ $schema['migrate_group'] = migrate_schema_group();
+ $schema['migrate_field_mapping'] = migrate_schema_field_mapping();
return $schema;
}
@@ -28,6 +30,12 @@ function migrate_schema_status() {
'not null' => TRUE,
'description' => 'Name of class to instantiate for this migration',
),
+ 'group_name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'Name of group containing migration',
+ ),
'status' => array(
'type' => 'int',
'size' => 'tiny',
@@ -115,6 +123,74 @@ function migrate_schema_log() {
);
}
+function migrate_schema_group() {
+ return array(
+ 'description' => 'Information on migration groups',
+ 'fields' => array(
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'Unique machine name for a migration group',
+ ),
+ 'title' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'Display name for a migration group',
+ ),
+ 'arguments' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of arguments to the migration group',
+ ),
+ ),
+ 'primary key' => array('name'),
+ );
+}
+
+function migrate_schema_field_mapping() {
+ return array(
+ 'description' => 'History of migration processes',
+ 'fields' => array(
+ 'fmid' => array(
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Unique ID for the field mapping row',
+ ),
+ 'machine_name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'Parent migration for the field mapping',
+ ),
+ 'destination_field' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'Destination field for the field mapping',
+ ),
+ 'source_field' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'Source field for the field mapping',
+ ),
+ 'options' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized MigrateFieldMapping object holding all options',
+ ),
+ ),
+ 'primary key' => array('fmid'),
+ );
+}
+
/**
* Implements hook_uninstall().
* Drop map/message tables, in case implementing classes did not.
@@ -137,13 +213,17 @@ function migrate_uninstall() {
->condition('module', 'migrate')
->execute();
}
+
+ // Remove variables
+ variable_del('migrate_disable_autoregistration');
+ variable_del('migrate_disabled_handlers');
+ variable_del('migrate_deprecation_warnings');
}
/**
* Add highwater mark
*/
function migrate_update_7001() {
- $ret = array();
if (!db_field_exists('migrate_status', 'highwater')) {
db_add_field('migrate_status', 'highwater', array(
'type' => 'varchar',
@@ -155,7 +235,7 @@ function migrate_update_7001() {
);
}
- $ret[] = t('Added highwater column to migrate_status table');
+ $ret = t('Added highwater column to migrate_status table');
return $ret;
}
@@ -163,7 +243,6 @@ function migrate_update_7001() {
* Add last_imported field to all map tables
*/
function migrate_update_7002() {
- $ret = array();
foreach (db_find_tables('migrate_map_%') as $tablename) {
if (!db_field_exists($tablename, 'last_imported')) {
db_add_field($tablename, 'last_imported', array(
@@ -175,7 +254,7 @@ function migrate_update_7002() {
));
}
}
- $ret[] = t('Added last_imported column to all map tables');
+ $ret = t('Added last_imported column to all map tables');
return $ret;
}
@@ -183,7 +262,7 @@ function migrate_update_7002() {
* Add lastthroughput column to migrate_status
*/
function migrate_update_7003() {
- $ret = array();
+ $ret = '';
if (!db_field_exists('migrate_status', 'lastthroughput')) {
db_add_field('migrate_status', 'lastthroughput', array(
'type' => 'int',
@@ -194,7 +273,7 @@ function migrate_update_7003() {
);
}
- $ret[] = t('Added lastthroughput column to migrate_status table');
+ $ret = t('Added lastthroughput column to migrate_status table');
return $ret;
}
@@ -202,7 +281,7 @@ function migrate_update_7003() {
* Convert lastimported datetime field to lastimportedtime int field.
*/
function migrate_update_7004() {
- $ret = array();
+ $ret = '';
if (!db_field_exists('migrate_status', 'lastimportedtime')) {
db_add_field('migrate_status', 'lastimportedtime', array(
'type' => 'int',
@@ -212,7 +291,7 @@ function migrate_update_7004() {
)
);
- if (!db_field_exists('migrate_status', 'lastimported')) {
+ if (db_field_exists('migrate_status', 'lastimported')) {
$result = db_select('migrate_status', 'ms')
->fields('ms', array('machine_name', 'lastimported'))
->execute();
@@ -226,7 +305,7 @@ function migrate_update_7004() {
db_drop_field('migrate_status', 'lastimported');
- $ret[] = t('Converted lastimported datetime field to lastimportedtime int field');
+ $ret .= "\n" . t('Converted lastimported datetime field to lastimportedtime int field');
}
}
return $ret;
@@ -236,11 +315,11 @@ function migrate_update_7004() {
* Add support for history logging
*/
function migrate_update_7005() {
- $ret = array();
+ $ret = '';
if (!db_table_exists('migrate_log')) {
- $ret[] = t('Create migrate_log table');
+ $ret .= "\n" . t('Create migrate_log table');
db_create_table('migrate_log', migrate_schema_log());
- $ret[] = t('Remove historic columns from migrate_status table');
+ $ret .= "\n" . t('Remove historic columns from migrate_status table');
db_drop_field('migrate_status', 'lastthroughput');
db_drop_field('migrate_status', 'lastimportedtime');
}
@@ -252,7 +331,7 @@ function migrate_update_7005() {
* dependencies or sourceMigration() must be changed! See CHANGELOG.txt.
*/
function migrate_update_7006() {
- $ret = array();
+ $ret = '';
if (!db_field_exists('migrate_status', 'class_name')) {
db_add_field('migrate_status', 'class_name', array(
'type' => 'varchar',
@@ -266,7 +345,7 @@ function migrate_update_7006() {
db_query("UPDATE {migrate_status}
SET class_name = CONCAT(machine_name, 'Migration')
");
- $ret[] = t('Added class_name column to migrate_status table');
+ $ret = t('Added class_name column to migrate_status table');
}
return $ret;
}
@@ -275,7 +354,7 @@ function migrate_update_7006() {
* Add arguments field to migrate_status table.
*/
function migrate_update_7007() {
- $ret = array();
+ $ret = '';
if (!db_field_exists('migrate_status', 'arguments')) {
db_add_field('migrate_status', 'arguments', array(
'type' => 'blob',
@@ -286,7 +365,7 @@ function migrate_update_7007() {
)
);
- $ret[] = t('Added arguments column to migrate_status table');
+ $ret = t('Added arguments column to migrate_status table');
}
return $ret;
}
@@ -298,10 +377,9 @@ function migrate_update_7008() {
// Updates can be run when the module is disabled, which would mean the
// call to migrate_migrations() will fail. Just bail in that case...
if (!module_exists('migrate')) {
- throw new DrupalUpdateException(t('This update cannot be run while the Migrate ' .
- 'module is disabled - you must enable Migrate to run this update.'));
+ throw new DrupalUpdateException(t('This update cannot be run while the Migrate module is disabled - you must enable Migrate to run this update.'));
}
- $ret = array();
+ $ret = '';
foreach (migrate_migrations() as $migration) {
if (is_a($migration, 'Migration')) {
// Since we're now tracking failed/ignored rows in the map table,
@@ -317,7 +395,7 @@ function migrate_update_7008() {
$field_schema['not null'] = FALSE;
$map_connection->schema()->changeField($map_table, $field, $field,
$field_schema);
- $ret[] = t('Changed !table.!field to be non-null',
+ $ret .= "\n" . t('Changed !table.!field to be non-null',
array('!table' => $map_table, '!field' => $field));
}
@@ -343,7 +421,7 @@ function migrate_update_7008() {
$msg_marked = TRUE;
}
if ($msg_marked) {
- $ret[] = t('Marked failures in !table', array('!table' => $map_table));
+ $ret .= "\n" . t('Marked failures in !table', array('!table' => $map_table));
}
}
}
@@ -363,10 +441,11 @@ function migrate_update_7201() {
}
/**
- * Add rollback_action field to all map tables
+ * Add rollback_action field to all map tables in the Drupal database.
*/
function migrate_update_7202() {
- $ret = array();
+ // Note this won't catch any prefixed tables, or any stored in the source
+ // database - ensureTables() will take care of those.
foreach (db_find_tables('migrate_map_%') as $tablename) {
if (!db_field_exists($tablename, 'rollback_action')) {
db_add_field($tablename, 'rollback_action', array(
@@ -379,6 +458,121 @@ function migrate_update_7202() {
));
}
}
- $ret[] = t('Added rollback_action column to all map tables');
+ $ret = t('Added rollback_action column to all map tables');
return $ret;
}
+
+/**
+ * Add database tracking of per-group info.
+ */
+function migrate_update_7203() {
+ $ret = '';
+ if (!db_table_exists('migrate_group')) {
+ $ret .= t('Create migrate_group table') . "\n";
+ db_create_table('migrate_group', migrate_schema_group());
+ }
+ if (!db_field_exists('migrate_status', 'group_name')) {
+ $ret .= t('Add group relationship to migrate_status table'). "\n";
+ db_add_field('migrate_status', 'group_name', array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => 'default',
+ 'description' => 'Name of group containing migration',
+ )
+ );
+ // Populate each migration's group_name field
+ $groups = array();
+ foreach (migrate_migrations() as $machine_name => $migration) {
+ $group_name = $migration->getGroup()->getName();
+ if (empty($group_name)) {
+ $group_name = 'default';
+ }
+ $groups[$group_name] = $group_name;
+ db_update('migrate_status')
+ ->fields(array('group_name' => $group_name))
+ ->condition('machine_name', $machine_name)
+ ->execute();
+ }
+ // Populate the migrate_group table
+ foreach ($groups as $group_name) {
+ $title = db_select('migrate_group', 'mg')
+ ->fields('mg', array('title'))
+ ->condition('name', $group_name)
+ ->execute()
+ ->fetchField();
+ if (!$title) {
+ db_insert('migrate_group')
+ ->fields(array(
+ 'name' => $group_name,
+ 'title' => $group_name,
+ 'arguments' => serialize(array()),
+ ))
+ ->execute();
+ }
+ }
+ }
+ return $ret;
+}
+
+/**
+ * Add database tracking of field mappings.
+ */
+function migrate_update_7204() {
+ $ret = '';
+ if (!db_table_exists('migrate_field_mapping')) {
+ $ret = t('Create migrate_field_mapping table');
+ db_create_table('migrate_field_mapping', migrate_schema_field_mapping());
+ }
+ return $ret;
+}
+
+/**
+ * Remove obsolete autoregistration disablement.
+ */
+function migrate_update_7205() {
+ variable_del('migrate_disable_autoregistration');
+}
+
+/**
+ * Replace three-column PK with a simple serial.
+ */
+function migrate_update_7206() {
+ if (!db_field_exists('migrate_field_mapping', 'fmid')) {
+ db_drop_primary_key('migrate_field_mapping');
+ db_add_field('migrate_field_mapping', 'fmid',
+ array(
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Unique ID for the field mapping row',
+ ),
+ array(
+ 'primary key' => array('fmid'),
+ )
+ );
+ }
+}
+
+/**
+ * Make sure we remove an empty 'default' group created by the previous updates.
+ */
+function migrate_update_7207() {
+ $rows = db_select('migrate_group', 'mg')
+ ->fields('mg', array('name'))
+ ->condition('name', 'default')
+ ->execute()
+ ->rowCount();
+ if ($rows > 0) {
+ $rows = db_select('migrate_status', 'ms')
+ ->fields('ms', array('machine_name'))
+ ->condition('group_name', 'default')
+ ->execute()
+ ->rowCount();
+ if ($rows == 0) {
+ db_delete('migrate_group')
+ ->condition('name', 'default')
+ ->execute();
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate.module b/sites/all/modules/contrib/migrate/migrate/migrate.module
index f48ae7c3..43e46d00 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate.module
+++ b/sites/all/modules/contrib/migrate/migrate/migrate.module
@@ -15,16 +15,24 @@
define('MIGRATE_API_VERSION', 2);
+define('MIGRATE_ACCESS_BASIC', 'migration information');
+define('MIGRATE_ACCESS_ADVANCED', 'advanced migration information');
+
/**
* Retrieve a list of all active migrations, ordered by dependencies. To be
* recognized, a class must be non-abstract, and derived from MigrationBase.
*
+ * @param $reset
+ * If TRUE, the static cache of migrations will be flushed before attempting to
+ * reinstantiate all active migrations. This can be important for script runs
+ * where migration classes may be dynamically registered.
+ *
* @return
* Array of migration objects, keyed by the machine name.
*/
-function migrate_migrations() {
+function migrate_migrations($reset = NULL) {
static $migrations = array();
- if (!empty($migrations)) {
+ if (!empty($migrations) && empty($reset)) {
return $migrations;
}
@@ -32,31 +40,30 @@ function migrate_migrations() {
// make sure any dynamic migrations defined in hook_migrate_api() get registered.
migrate_get_module_apis(TRUE);
+ $dependencies_list = array();
$dependent_migrations = array();
$required_migrations = array();
$result = db_select('migrate_status', 'ms')
- ->fields('ms', array('machine_name', 'class_name', 'arguments'))
+ ->fields('ms', array('machine_name', 'class_name'))
->execute();
foreach ($result as $row) {
if (class_exists($row->class_name)) {
$reflect = new ReflectionClass($row->class_name);
if (!$reflect->isAbstract() && $reflect->isSubclassOf('MigrationBase')) {
- $arguments = unserialize($row->arguments);
- if (!$arguments || !is_array($arguments)) {
- $arguments = array();
- }
- $migration = MigrationBase::getInstance($row->machine_name,
- $row->class_name, $arguments);
- $dependencies = $migration->getDependencies();
- if (count($dependencies) > 0) {
- // Set classes with dependencies aside for reordering
- $dependent_migrations[$row->machine_name] = $migration;
- $required_migrations += $dependencies;
- }
- else {
- // No dependencies, just add
- $migrations[$row->machine_name] = $migration;
+ $migration = MigrationBase::getInstance($row->machine_name);
+ if ($migration) {
+ $dependencies = $migration->getDependencies();
+ $dependencies_list[$row->machine_name] = $dependencies;
+ if (count($dependencies) > 0) {
+ // Set classes with dependencies aside for reordering
+ $dependent_migrations[$row->machine_name] = $migration;
+ $required_migrations += $dependencies;
+ }
+ else {
+ // No dependencies, just add
+ $migrations[$row->machine_name] = $migration;
+ }
}
}
else {
@@ -70,32 +77,10 @@ function migrate_migrations() {
}
}
- // Scan modules with dependencies - we'll take 20 passes at it before
- // giving up
- // TODO: Can we share code with _migrate_class_list()?
- $iterations = 0;
- while (count($dependent_migrations) > 0) {
- if ($iterations++ > 20) {
- $migration_names = implode(',', array_keys($dependent_migrations));
- throw new MigrateException(t('Failure to sort migration list - most likely due ' .
- 'to circular dependencies involving !migration_names',
- array('!migration_names' => $migration_names)));
- }
- foreach ($dependent_migrations as $name => $migration) {
- $ready = TRUE;
- // Scan all the dependencies for this class and make sure they're all
- // in the final list
- foreach ($migration->getDependencies() as $dependency) {
- if (!isset($migrations[$dependency])) {
- $ready = FALSE;
- break;
- }
- }
- if ($ready) {
- // Yes they are! Move this class to the final list
- $migrations[$name] = $migration;
- unset($dependent_migrations[$name]);
- }
+ $ordered_migrations = migrate_order_dependencies($dependencies_list);
+ foreach ($ordered_migrations as $name) {
+ if (!isset($migrations[$name])) {
+ $migrations[$name] = $dependent_migrations[$name];
}
}
@@ -123,59 +108,6 @@ function migrate_migrations() {
return $migrations;
}
-/**
- * On request, scan the Drupal code registry for any new migration classes
- * for us to register in migrate_status.
- */
-function migrate_autoregister() {
- // Make sure the registry is up-to-date on all available classes.
- require_once 'includes/registry.inc';
- _registry_update();
-
- // Get list of modules implementing Migrate API
- $modules = array_keys(migrate_get_module_apis(TRUE));
-
- // Get list of classes we already know about
- $existing_classes = db_select('migrate_status', 'ms')
- ->fields('ms', array('class_name'))
- ->execute()
- ->fetchCol();
-
- // Discover class names registered with Drupal by modules implementing our API
- $result = db_select('registry', 'r')
- ->fields('r', array('name'))
- ->condition('type', 'class')
- ->condition('module', $modules, 'IN')
- ->condition('filename', '%.test', 'NOT LIKE')
- ->execute();
-
- foreach ($result as $record) {
- $class_name = $record->name;
- // If we already know about this class, skip it
- if (isset($existing_classes[$class_name])) {
- continue;
- }
-
- // Validate it's an implemented subclass of the parent class
- // Ignore errors
- try {
- $class = new ReflectionClass($class_name);
- }
- catch (Exception $e) {
- continue;
- }
- if (!$class->isAbstract() && $class->isSubclassOf('MigrationBase')) {
- // Verify that it's not a dynamic class (the implementor will be responsible
- // for registering those).
- $dynamic = call_user_func(array($class_name, 'isDynamic'));
- if (!$dynamic) {
- // OK, this is a new non-dynamic migration class, register it
- MigrationBase::registerMigration($class_name);
- }
- }
- }
-}
-
/**
* Invoke any available handlers attached to a given destination type.
* If any handlers have dependencies defined, they will be invoked after
@@ -216,8 +148,6 @@ function migrate_handler_invoke_all($destination, $method) {
/**
* Invoke any available handlers attached to a given field type.
- * If any handlers have dependencies defined, they will be invoked after
- * the specified handlers.
*
* @param $entity
* The object we are building up before calling example_save().
@@ -231,18 +161,21 @@ function migrate_handler_invoke_all($destination, $method) {
* @param $method
* Handler method to call (defaults to prepare()).
*/
-function migrate_field_handler_invoke_all($entity, array $field_info, array $instance,
- array $values, $method = 'prepare') {
+function migrate_field_handler_invoke_all($entity, array $field_info,
+ array $instance, array $values, $method = 'prepare') {
$return = array();
$type = $field_info['type'];
$class_list = _migrate_class_list('MigrateFieldHandler');
- $disabled = unserialize(variable_get('migrate_disabled_handlers', serialize(array())));
+ $disabled = unserialize(variable_get('migrate_disabled_handlers',
+ serialize(array())));
+ $handler_called = FALSE;
foreach ($class_list as $class_name => $handler) {
if (!in_array($class_name, $disabled) && $handler->handlesType($type)
&& method_exists($handler, $method)) {
migrate_instrument_start($class_name . '->' . $method);
$result = call_user_func_array(array($handler, $method),
array($entity, $field_info, $instance, $values));
+ $handler_called = TRUE;
migrate_instrument_stop($class_name . '->' . $method);
if (isset($result) && is_array($result)) {
$return = array_merge_recursive($return, $result);
@@ -252,6 +185,20 @@ function migrate_field_handler_invoke_all($entity, array $field_info, array $ins
}
}
}
+
+ if (!$handler_called && $method == 'prepare') {
+ $handler = new MigrateDefaultFieldHandler();
+ migrate_instrument_start('MigrateDefaultFieldHandler->prepare');
+ $result = call_user_func_array(array($handler, 'prepare'),
+ array($entity, $field_info, $instance, $values));
+ migrate_instrument_stop('MigrateDefaultFieldHandler->prepare');
+ if (isset($result) && is_array($result)) {
+ $return = array_merge_recursive($return, $result);
+ }
+ elseif (isset($result)) {
+ $return[] = $result;
+ }
+ }
return $return;
}
@@ -269,10 +216,9 @@ function migrate_field_handler_invoke_all($entity, array $field_info, array $ins
function _migrate_class_list($parent_class) {
// Get info on modules implementing Migrate API
static $module_info;
- if (!isset($modules)) {
+ if (!isset($module_info)) {
$module_info = migrate_get_module_apis();
}
- $modules = array_keys($module_info);
static $class_lists = array();
if (!isset($class_lists[$parent_class])) {
@@ -291,90 +237,6 @@ function _migrate_class_list($parent_class) {
}
}
}
- // Avoid scrounging the registry for handler classes if possible.
- if (variable_get('migrate_disable_autoregistration', FALSE)) {
- return $class_lists[$parent_class];
- }
- $dependent_classes = array();
- $required_classes = array();
- // Discover class names registered with Drupal by modules implementing our API
- $result = db_select('registry', 'r')
- ->fields('r', array('name'))
- ->condition('type', 'class')
- ->condition('module', $modules, 'IN')
- ->condition('filename', '%.test', 'NOT LIKE')
- ->execute();
-
- foreach ($result as $record) {
- // Validate it's an implemented subclass of the parent class
- // We can get funky errors here, ignore them (and the class that caused them)
- try {
- $class = new ReflectionClass($record->name);
- }
- catch (Exception $e) {
- continue;
- }
- if (!$class->isAbstract() && $class->isSubclassOf($parent_class)) {
- // If the constructor has required parameters, this may fail. We will
- // silently ignore - it is up to the implementor of such a class to
- // instantiate it in hook_migrations_alter().
- try {
- $object = new $record->name;
- }
- catch (Exception $e) {
- unset($object);
- }
- if (isset($object)) {
- $dependencies = $object->getDependencies();
- if (count($dependencies) > 0) {
- // Set classes with dependencies aside for reordering
- $dependent_classes[$record->name] = $object;
- $required_classes += $dependencies;
- }
- else {
- // No dependencies, just add
- $class_lists[$parent_class][$record->name] = $object;
- }
- }
- }
- }
-
- // Validate that each depended-on class at least exists
- foreach ($required_classes as $class_name) {
- if ((!isset($dependent_classes[$class_name])) && !isset($class_lists[$parent_class][$class_name])) {
- throw new MigrateException(t('Dependency on non-existent class !class - make sure ' .
- 'you have added the file defining !class to the .info file.',
- array('!class' => $class_name)));
- }
- }
-
- // Scan modules with dependencies - we'll take 20 passes at it before
- // giving up
- $iterations = 0;
- while (count($dependent_classes) > 0) {
- if ($iterations++ > 20) {
- $class_names = implode(',', array_keys($dependent_classes));
- throw new MigrateException(t('Failure to sort class list - most likely due ' .
- 'to circular dependencies involving !class_names.',
- array('!class_names' => $class_names)));
- }
- foreach ($dependent_classes as $name => $object) {
- $ready = TRUE;
- // Scan all the dependencies for this class and make sure they're all
- // in the final list
- foreach ($object->getDependencies() as $dependency) {
- if (!isset($class_lists[$parent_class][$dependency])) {
- $ready = FALSE;
- break;
- }
- }
- if ($ready) {
- // Yes they are! Move this class to the final list
- $class_lists[$parent_class][$name] = $object;
- unset($dependent_classes[$name]);
- }
- }
- }
}
return $class_lists[$parent_class];
}
@@ -387,9 +249,26 @@ function migrate_hook_info() {
$hooks['migrate_api'] = array(
'group' => 'migrate',
);
+ $hooks['migrate_api_alter'] = array(
+ 'group' => 'migrate',
+ );
return $hooks;
}
+/**
+ * Implementation of hook_permission().
+ */
+function migrate_permission() {
+ return array(
+ MIGRATE_ACCESS_BASIC => array(
+ 'title' => t('Access to basic migration information'),
+ ),
+ MIGRATE_ACCESS_ADVANCED => array(
+ 'title' => t('Access to advanced migration information'),
+ ),
+ );
+}
+
/**
* Get a list of modules that support the current migrate API.
*/
@@ -405,13 +284,6 @@ function migrate_get_module_apis($reset = FALSE) {
$info = $function();
if (isset($info['api']) && $info['api'] == MIGRATE_API_VERSION) {
$cache[$module] = $info;
- // Register any migrations defined via the hook.
- if (isset($info['migrations']) && is_array($info['migrations'])) {
- foreach ($info['migrations'] as $machine_name => $arguments) {
- MigrationBase::registerMigration($arguments['class_name'],
- $machine_name, $arguments);
- }
- }
}
else {
drupal_set_message(t('%function supports Migrate API version %modversion,
@@ -420,11 +292,97 @@ function migrate_get_module_apis($reset = FALSE) {
'%version' => MIGRATE_API_VERSION)));
}
}
+
+ // Allow modules to alter the migration information.
+ drupal_alter('migrate_api', $cache);
}
return $cache;
}
+/**
+ * Register any migrations defined in hook_migrate_api().
+ *
+ * @param array $machine_names
+ * If populated, only (re)register the specified migrations.
+ */
+function migrate_static_registration($machine_names = array()) {
+ $module_info = migrate_get_module_apis(TRUE);
+ foreach ($module_info as $module => $info) {
+ // Register any groups defined via the hook.
+ if (isset($info['groups']) && is_array($info['groups'])) {
+ foreach ($info['groups'] as $name => $arguments) {
+ $title = $arguments['title'];
+ unset($arguments['title']);
+ MigrateGroup::register($name, $title, $arguments);
+ }
+ }
+ // Register any migrations defined via the hook.
+ if (isset($info['migrations']) && is_array($info['migrations'])) {
+ foreach ($info['migrations'] as $machine_name => $arguments) {
+ // If we have an explicit list to register, skip any not in the list.
+ if (!empty($machine_names) && !in_array($machine_name, $machine_names)) {
+ continue;
+ }
+ $class_name = $arguments['class_name'];
+ unset($arguments['class_name']);
+ // Call the right registerMigration implementation. Note that this means
+ // that classes that override registerMigration() must always call it
+ // directly, they cannot register those classes by defining them in
+ // hook_migrate_api() and expect their extension to be called.
+ if (is_subclass_of($class_name, 'Migration')) {
+ Migration::registerMigration($class_name, $machine_name, $arguments);
+ }
+ else {
+ MigrationBase::registerMigration($class_name, $machine_name, $arguments);
+ }
+ }
+ }
+
+ }
+}
+
+/**
+ * Do a topological sort on our dependencies graph.
+ */
+function migrate_order_dependencies($dependencies) {
+ $visited = array();
+ $list = array();
+
+ foreach (array_keys($dependencies) as $name) {
+ $visited[$name] = FALSE;
+ }
+
+ foreach (array_keys($dependencies) as $name) {
+ migrate_visit_dependent($dependencies, $name, $list, $visited);
+ }
+
+ return $list;
+}
+
+/**
+ * Depth-first search for independent migrations.
+ */
+function migrate_visit_dependent($dependencies, $name, &$list, &$visited) {
+ if ($visited[$name]) {
+ if ($list[$name]) {
+ return;
+ }
+ else {
+ throw new MigrateException(t('Failure to sort migration list due to circular dependencies involving %name.', array('%name' => $name)));
+ }
+ }
+
+ $visited[$name] = TRUE;
+ if (isset($dependencies[$name])) {
+ foreach ($dependencies[$name] as $dependent) {
+ migrate_visit_dependent($dependencies, $dependent, $list, $visited);
+ }
+ }
+
+ $list[$name] = $name;
+}
+
/**
* Implements hook_watchdog().
* Find the migration that is currently running and notify it.
@@ -579,3 +537,28 @@ function migrate_overview() {
}
return $overview;
}
+
+/**
+ * Implements hook_modules_enabled.
+ */
+function migrate_modules_enabled($modules) {
+ if (array_intersect($modules, module_implements('migrate_api'))) {
+ migrate_static_registration();
+ }
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ */
+function migrate_module_implements_alter(&$implementation, $hook) {
+ // Ensure that the Migration class exists, as different bootstrap phases may
+ // not have included migration.inc yet.
+ if (class_exists('Migration') && $migration = Migration::currentMigration()) {
+ $disable_hooks = $migration->getDisableHooks();
+ if (isset($disable_hooks[$hook])) {
+ foreach ($disable_hooks[$hook] as $module) {
+ unset($implementation[$module]);
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.inc
index ebf90095..02e3a244 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.inc
@@ -16,34 +16,41 @@
* - Comments to be attached to the beer nodes are described in the source
* migrate_example_beer_comment table.
*
- * We will use the Migrate API to import and transform this data and turn it into
- * a working Drupal system.
+ * We will use the Migrate API to import and transform this data and turn it
+ * into a working Drupal site.
*/
/**
* To define a migration process from a set of source data to a particular
* kind of Drupal object (for example, a specific node type), you define
* a class derived from Migration. You must define a constructor to initialize
- * your migration object. By default, your class name will be the "machine name"
- * of the migration, by which you refer to it. Note that the machine name is
- * case-sensitive.
+ * your migration object.
+ *
+ * For your classes to be instantiated so they can be used to import content,
+ * you must register them - look at migrate_example.migrate.inc to see how
+ * registration works. Right now, it's important to understand that each
+ * migration will have a unique "machine name", which is displayed in the UI
+ * and is used to reference the migration in drush commands.
*
* In any serious migration project, you will find there are some options
* which are common to the individual migrations you're implementing. You can
* define an abstract intermediate class derived from Migration, then derive your
* individual migrations from that, to share settings, utility functions, etc.
*/
-abstract class BasicExampleMigration extends DynamicMigration {
- public function __construct() {
- // Always call the parent constructor first for basic setup
- parent::__construct();
+abstract class BasicExampleMigration extends Migration {
+ // A Migration constructor takes an array of arguments as its first parameter.
+ // The arguments must be passed through to the parent constructor.
+ public function __construct($arguments) {
+ parent::__construct($arguments);
// With migrate_ui enabled, migration pages will indicate people involved in
// the particular migration, with their role and contact info. We default the
// list in the shared class; it can be overridden for specific migrations.
$this->team = array(
- new MigrateTeamMember('Liz Taster', 'ltaster@example.com', t('Product Owner')),
- new MigrateTeamMember('Larry Brewer', 'lbrewer@example.com', t('Implementor')),
+ new MigrateTeamMember('Liz Taster', 'ltaster@example.com',
+ t('Product Owner')),
+ new MigrateTeamMember('Larry Brewer', 'lbrewer@example.com',
+ t('Implementor')),
);
// Individual mappings in a migration can be linked to a ticket or issue
@@ -62,23 +69,45 @@ abstract class BasicExampleMigration extends DynamicMigration {
* this will receive data that originated from the source and has been mapped
* by the Migration class, and create Drupal objects.
* $this->map - An instance of a class derived from MigrateMap, this will keep
- * track of which source items have been imported and what destination objects
- * they map to.
- * Mappings - Use $this->addFieldMapping to tell the Migration class what source
- * fields correspond to what destination fields, and additional information
- * associated with the mappings.
+ * track of which source items have been imported and what destination
+ * objects they map to.
+ * Field mappings - Use $this->addFieldMapping to tell the Migration class what
+ * source fields correspond to what destination fields, and additional
+ * information associated with the mappings.
*/
class BeerTermMigration extends BasicExampleMigration {
- public function __construct() {
- parent::__construct();
- // Human-friendly description of your migration process. Be as detailed as you
- // like.
- $this->description = t('Migrate styles from the source database to taxonomy terms');
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ // Human-friendly description of your migration process. Be as detailed as
+ // you like.
+ $this->description =
+ t('Migrate styles from the source database to taxonomy terms');
+
+ // In this example, we're using tables that have been added to the existing
+ // Drupal database but which are not Drupal tables. You can examine the
+ // various tables (starting here with migrate_example_beer_topic) using a
+ // database browser such as phpMyAdmin.
+ // First, we set up a query for this data. Note that by ordering on
+ // style_parent, we guarantee root terms are migrated first, so the
+ // parent_name mapping below will find that the parent exists.
+ $query = db_select('migrate_example_beer_topic', 'met')
+ ->fields('met', array('style', 'details', 'style_parent', 'region',
+ 'hoppiness'))
+ // This sort assures that parents are saved before children.
+ ->orderBy('style_parent', 'ASC');
+
+ // Create a MigrateSource object, which manages retrieving the input data.
+ $this->source = new MigrateSourceSQL($query);
+
+ // Set up our destination - terms in the migrate_example_beer_styles
+ // vocabulary (note that we pass the machine name of the vocabulary).
+ $this->destination =
+ new MigrateDestinationTerm('migrate_example_beer_styles');
// Create a map object for tracking the relationships between source rows
- // and their resulting Drupal objects. Usually, you'll use the MigrateSQLMap
- // class, which uses database tables for tracking. Pass the machine name
- // (BeerTerm) of this migration to use in generating map and message tables.
+ // and their resulting Drupal objects. We will use the MigrateSQLMap class,
+ // which uses database tables for tracking. Pass the machine name (BeerTerm)
+ // of this migration to use in generating map and message table names.
// And, pass schema definitions for the primary keys of the source and
// destination - we need to be explicit for our source, but the destination
// class knows its schema already.
@@ -93,28 +122,11 @@ class BeerTermMigration extends BasicExampleMigration {
MigrateDestinationTerm::getKeySchema()
);
- // In this example, we're using tables that have been added to the existing
- // Drupal database but which are not Drupal tables. You can examine the
- // various tables (starting here with migrate_example_beer_topic) using a
- // database browser like phpMyAdmin.
- // First, we set up a query for this data. Note that by ordering on
- // style_parent, we guarantee root terms are migrated first, so the
- // parent_name mapping below will find that the parent exists.
- $query = db_select('migrate_example_beer_topic', 'met')
- ->fields('met', array('style', 'details', 'style_parent', 'region', 'hoppiness'))
- // This sort assures that parents are saved before children.
- ->orderBy('style_parent', 'ASC');
-
- // Create a MigrateSource object, which manages retrieving the input data.
- $this->source = new MigrateSourceSQL($query);
-
- // Set up our destination - terms in the migrate_example_beer_styles vocabulary
- $this->destination = new MigrateDestinationTerm('migrate_example_beer_styles');
-
// Assign mappings TO destination fields FROM source fields. To discover
// the names used in these calls, use the drush commands
- // drush migrate-fields-destination BeerTerm
- // drush migrate-fields-source BeerTerm
+ // drush migrate-fields-destination BeerTerm
+ // drush migrate-fields-source BeerTerm
+ // or review the detail pages in the UI.
$this->addFieldMapping('name', 'style');
$this->addFieldMapping('description', 'details');
@@ -127,12 +139,13 @@ class BeerTermMigration extends BasicExampleMigration {
// migration info page when the migrate_ui module is enabled. The default
// is 'Done', indicating active mappings which need no attention. A
// suggested practice is to use groups of:
- // Do Not Migrate (or DNM) to indicate source fields which are not being used,
- // or destination fields not to be populated by migration.
+ // Do Not Migrate (or DNM) to indicate source fields which are not being
+ // used, or destination fields not to be populated by migration.
// Client Issues to indicate input from the client is needed to determine
// how a given field is to be migrated.
// Implementor Issues to indicate that the client has provided all the
- // necessary information, and now the implementor needs to complete the work.
+ // necessary information, and now the implementor needs to complete the
+ // work.
$this->addFieldMapping(NULL, 'hoppiness')
->description(t('This info will not be maintained in Drupal'))
->issueGroup(t('DNM'));
@@ -141,10 +154,11 @@ class BeerTermMigration extends BasicExampleMigration {
// MigrateFieldMapping::ISSUE_PRIORITY_OK). If you're using an issue
// tracking system, and have defined issuePattern (see BasicExampleMigration
// above), you can specify a ticket/issue number in the system on the
- // mapping and migrate_ui will link directory to it.
+ // mapping and migrate_ui will link directly to it.
$this->addFieldMapping(NULL, 'region')
->description('Will a field be added to the vocabulary for this?')
->issueGroup(t('Client Issues'))
+ // This priority wil cause the mapping to be highlighted in the UI.
->issuePriority(MigrateFieldMapping::ISSUE_PRIORITY_MEDIUM)
->issueNumber(770064);
@@ -152,8 +166,8 @@ class BeerTermMigration extends BasicExampleMigration {
// explicitly - this makes sure that everyone understands exactly what is
// being migrated and what is not. Also, migrate_ui highlights unmapped
// fields, or mappings involving fields not in the source and destination,
- // so if (for example) a new field is added to the destination field it's
- // immediately visible, and you can find out if anything needs to be
+ // so if (for example) a new field is added to the destination typ it's
+ // immediately visible, and you can decide if anything needs to be
// migrated into it.
$this->addFieldMapping('format')
->issueGroup(t('DNM'));
@@ -163,11 +177,12 @@ class BeerTermMigration extends BasicExampleMigration {
->issueGroup(t('DNM'));
// We conditionally DNM these fields, so your field mappings will be clean
- // whether or not you have path and or pathauto enabled
- if (module_exists('path')) {
+ // whether or not you have path and/or pathauto enabled.
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
$this->addFieldMapping('path')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
+ if (isset($destination_fields['pathauto'])) {
$this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
}
@@ -186,10 +201,15 @@ class BeerTermMigration extends BasicExampleMigration {
* transformations of the data.
*/
class BeerUserMigration extends BasicExampleMigration {
- public function __construct() {
+ public function __construct($arguments) {
// The basic setup is similar to BeerTermMigraiton
- parent::__construct();
+ parent::__construct($arguments);
$this->description = t('Beer Drinkers of the world');
+ $query = db_select('migrate_example_beer_account', 'mea')
+ ->fields('mea', array('aid', 'status', 'posted', 'name',
+ 'nickname', 'password', 'mail', 'sex', 'beers'));
+ $this->source = new MigrateSourceSQL($query);
+ $this->destination = new MigrateDestinationUser();
$this->map = new MigrateSQLMap($this->machineName,
array('aid' => array(
'type' => 'int',
@@ -199,18 +219,15 @@ class BeerUserMigration extends BasicExampleMigration {
),
MigrateDestinationUser::getKeySchema()
);
- $query = db_select('migrate_example_beer_account', 'mea')
- ->fields('mea', array('aid', 'status', 'posted', 'name', 'nickname', 'password', 'mail', 'sex', 'beers'));
- $this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationUser();
- // One good way to organize your mappings is in three groups - mapped fields,
- // unmapped source fields, and unmapped destination fields
+ // One good way to organize your mappings in the constructor is in three
+ // groups - mapped fields, unmapped source fields, and unmapped destination
+ // fields.
// Mapped fields
// This is a shortcut you can use when the source and destination field
- // names are identical (i.e., the email address field is named 'mail' in
+ // names are identical (e.g., the email address field is named 'mail' in
// both the source table and in Drupal).
$this->addSimpleMappings(array('status', 'mail'));
@@ -224,7 +241,8 @@ class BeerUserMigration extends BasicExampleMigration {
$this->addFieldMapping('name', 'name')
->dedupe('users', 'name');
- // The migrate module automatically converts date/time strings to UNIX timestamps.
+ // The migrate module automatically converts date/time strings to UNIX
+ // timestamps.
$this->addFieldMapping('created', 'posted');
$this->addFieldMapping('pass', 'password');
@@ -246,6 +264,10 @@ class BeerUserMigration extends BasicExampleMigration {
$this->addFieldMapping('field_migrate_example_favbeers', 'beers')
->separator('|');
}
+ else {
+ $this->addFieldMapping(NULL, 'beers')
+ ->issueGroup(t('DNM'));
+ }
// Unmapped source fields
$this->addFieldMapping(NULL, 'nickname')
@@ -254,10 +276,20 @@ class BeerUserMigration extends BasicExampleMigration {
// Unmapped destination fields
// This is a shortcut you can use to mark several destination fields as DNM
- // at once
- $this->addUnmigratedDestinations(array('theme', 'signature', 'access', 'login',
- 'timezone', 'language', 'picture', 'is_new', 'signature_format', 'role_names',
- 'init', 'data'));
+ // at once.
+ $this->addUnmigratedDestinations(array(
+ 'access',
+ 'data',
+ 'is_new',
+ 'language',
+ 'login',
+ 'picture',
+ 'role_names',
+ 'signature',
+ 'signature_format',
+ 'theme',
+ 'timezone',
+ ));
// Oops, we made a typo - this should have been 'init'! If you have
// migrate_ui enabled, look at the BeerUser info page - you'll see that it
@@ -267,10 +299,11 @@ class BeerUserMigration extends BasicExampleMigration {
$this->addFieldMapping('int')
->issueGroup(t('DNM'));
- if (module_exists('path')) {
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
$this->addFieldMapping('path')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
+ if (isset($destination_fields['pathauto'])) {
$this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
}
@@ -283,35 +316,19 @@ class BeerUserMigration extends BasicExampleMigration {
* and creates Drupal nodes of type 'Beer' as destination.
*/
class BeerNodeMigration extends BasicExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Beers of the world');
- // You may optionally declare dependencies for your migration - other migrations
- // which should run first. In this case, terms assigned to our nodes and
- // the authors of the nodes should be migrated before the nodes themselves.
- $this->dependencies = array('BeerTerm', 'BeerUser');
-
- $this->map = new MigrateSQLMap($this->machineName,
- array(
- 'bid' => array(
- 'type' => 'int',
- 'not null' => TRUE,
- 'description' => 'Beer ID.',
- 'alias' => 'b',
- )
- ),
- MigrateDestinationNode::getKeySchema()
- );
-
// We have a more complicated query. The Migration class fundamentally
// depends on taking a single source row and turning it into a single
// Drupal object, so how do we deal with zero or more terms attached to
- // each node? One way (demonstrated for MySQL) is to pull them into a single
+ // each node? One way (valid for MySQL only) is to pull them into a single
// comma-separated list.
$query = db_select('migrate_example_beer_node', 'b')
- ->fields('b', array('bid', 'name', 'body', 'excerpt', 'aid', 'countries',
- 'image', 'image_alt', 'image_title', 'image_description'));
+ ->fields('b', array('bid', 'name', 'body', 'excerpt', 'aid',
+ 'countries', 'image', 'image_alt', 'image_title',
+ 'image_description'));
$query->leftJoin('migrate_example_beer_topic_node', 'tb', 'b.bid = tb.bid');
// Gives a single comma-separated list of related terms
$query->groupBy('tb.bid');
@@ -331,6 +348,18 @@ class BeerNodeMigration extends BasicExampleMigration {
// Set up our destination - nodes of type migrate_example_beer
$this->destination = new MigrateDestinationNode('migrate_example_beer');
+ $this->map = new MigrateSQLMap($this->machineName,
+ array(
+ 'bid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'description' => 'Beer ID.',
+ 'alias' => 'b',
+ )
+ ),
+ MigrateDestinationNode::getKeySchema()
+ );
+
// Mapped fields
$this->addFieldMapping('title', 'name')
->description(t('Mapping beer name in source to node title'));
@@ -340,15 +369,6 @@ class BeerNodeMigration extends BasicExampleMigration {
->issueNumber(765736)
->issuePriority(MigrateFieldMapping::ISSUE_PRIORITY_LOW);
- // To maintain node identities between the old and new systems (i.e., have
- // the same unique IDs), map the ID column from the old system to nid and
- // set is_new to TRUE. This works only if we're importing into a system that
- // has no existing nodes with the nids being imported.
- $this->addFieldMapping('nid', 'bid')
- ->description(t('Preserve old beer ID as nid in Drupal'));
- $this->addFieldMapping('is_new')
- ->defaultValue(TRUE);
-
// References to related objects (such as the author of the content) are
// most likely going to be identifiers from the source data, not Drupal
// identifiers (such as uids). You can use the mapping from the relevant
@@ -357,25 +377,37 @@ class BeerNodeMigration extends BasicExampleMigration {
// find a corresponding uid for the aid, the owner will be the administrative
// account.
$this->addFieldMapping('uid', 'aid')
+ // Note this is the machine name of the user migration.
->sourceMigration('BeerUser')
->defaultValue(1);
- // This is a multi-value text field
+ // This is a multi-value text field - in the source data the values are
+ // separated by |, so we tell migrate to split it by that character.
$this->addFieldMapping('field_migrate_example_country', 'countries')
->separator('|');
- // These are related terms, which by default will be looked up by name
+ // These are related terms, which by default will be looked up by name.
$this->addFieldMapping('migrate_example_beer_styles', 'terms')
->separator(',');
- // Some fields may have subfields such as text formats or summaries
- // (equivalent to teasers in previous Drupal versions).
- // These can be individually mapped as we see here.
+ // Some fields may have subfields such as text formats or summaries. These
+ // can be individually mapped as we see here.
$this->addFieldMapping('body', 'body');
$this->addFieldMapping('body:summary', 'excerpt');
- // Copy an image file, write DB record to files table, and save in Field storage.
- // We map the filename (relative to the source_dir below) to the field itself.
+ // File fields are more complex - the file needs to be copied, a Drupal
+ // file entity (file_managed table row) created, and the field populated.
+ // There are several different options involved. It's usually best to do
+ // migrate the files themselves in their own migration (see wine.inc for an
+ // example), but they can also be brought over through the field mapping.
+
+ // We map the filename (relative to the source_dir below) to the field
+ // itself.
$this->addFieldMapping('field_migrate_example_image', 'image');
+ // The file_class determines how the 'image' value is interpreted, and what
+ // other options are available. In this case, MigrateFileUri indicates that
+ // the 'image' value is a URI.
+ $this->addFieldMapping('field_migrate_example_image:file_class')
+ ->defaultValue('MigrateFileUri');
// Here we specify the directory containing the source files.
$this->addFieldMapping('field_migrate_example_image:source_dir')
->defaultValue(drupal_get_path('module', 'migrate_example'));
@@ -387,24 +419,46 @@ class BeerNodeMigration extends BasicExampleMigration {
$this->addUnmigratedSources(array('image_description'));
// Unmapped destination fields
- $this->addUnmigratedDestinations(array('created', 'changed', 'status',
- 'promote', 'revision', 'language', 'revision_uid', 'log', 'tnid',
- 'body:format', 'body:language', 'migrate_example_beer_styles:source_type',
- 'migrate_example_beer_styles:create_term', 'field_migrate_example_image:destination_dir',
- 'field_migrate_example_image:language', 'field_migrate_example_image:file_replace',
- 'field_migrate_example_image:preserve_files', 'field_migrate_example_country:language', 'comment',
- 'field_migrate_example_image:file_class', 'field_migrate_example_image:destination_file'));
+ // Some conventions we use here: with a long list of fields to ignore, we
+ // arrange them alphabetically, one distinct field per line (although
+ // subfields of the same field may be grouped on the same line), and indent
+ // subfields to distinguish them from top-level fields.
+ $this->addUnmigratedDestinations(array(
+ 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'field_migrate_example_country:language',
+ 'field_migrate_example_image:destination_dir',
+ 'field_migrate_example_image:destination_file',
+ 'field_migrate_example_image:file_replace',
+ 'field_migrate_example_image:language',
+ 'field_migrate_example_image:preserve_files',
+ 'field_migrate_example_image:urlencode',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_beer_styles:source_type',
+ 'migrate_example_beer_styles:create_term',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'tnid',
+ ));
- if (module_exists('path')) {
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
$this->addFieldMapping('path')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
+ if (isset($destination_fields['pathauto'])) {
$this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
}
}
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
}
@@ -414,10 +468,21 @@ class BeerNodeMigration extends BasicExampleMigration {
* Drupal comment objects.
*/
class BeerCommentMigration extends BasicExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = 'Comments about beers';
- $this->dependencies = array('BeerUser', 'BeerNode');
+
+ $query = db_select('migrate_example_beer_comment', 'mec')
+ ->fields('mec', array('cid', 'cid_parent', 'name', 'mail', 'aid',
+ 'body', 'bid', 'subject'))
+ ->orderBy('cid_parent', 'ASC');
+ $this->source = new MigrateSourceSQL($query);
+ // Note that the machine name passed for comment migrations is
+ // 'comment_node_' followed by the machine name of the node type these
+ // comments are attached to.
+ $this->destination =
+ new MigrateDestinationComment('comment_node_migrate_example_beer');
+
$this->map = new MigrateSQLMap($this->machineName,
array('cid' => array(
'type' => 'int',
@@ -426,19 +491,14 @@ class BeerCommentMigration extends BasicExampleMigration {
),
MigrateDestinationComment::getKeySchema()
);
- $query = db_select('migrate_example_beer_comment', 'mec')
- ->fields('mec', array('cid', 'cid_parent', 'name', 'mail', 'aid', 'body', 'bid', 'subject'))
- ->orderBy('cid_parent', 'ASC');
- $this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationComment('comment_node_migrate_example_beer');
// Mapped fields
$this->addSimpleMappings(array('name', 'subject', 'mail'));
$this->addFieldMapping('status')
->defaultValue(COMMENT_PUBLISHED);
- // We preserved bid => nid ids during BeerNode import so simple mapping suffices.
- $this->addFieldMapping('nid', 'bid');
+ $this->addFieldMapping('nid', 'bid')
+ ->sourceMigration('BeerNode');
$this->addFieldMapping('uid', 'aid')
->sourceMigration('BeerUser')
@@ -453,7 +513,24 @@ class BeerCommentMigration extends BasicExampleMigration {
// No unmapped source fields
// Unmapped destination fields
- $this->addUnmigratedDestinations(array('hostname', 'created', 'changed',
- 'thread', 'homepage', 'language', 'comment_body:format', 'comment_body:language'));
+ $this->addUnmigratedDestinations(array(
+ 'changed',
+ 'comment_body:format', 'comment_body:language',
+ 'created',
+ 'homepage',
+ 'hostname',
+ 'language',
+ 'thread',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
+ $this->addFieldMapping('path')
+ ->issueGroup(t('DNM'));
+ if (isset($destination_fields['pathauto'])) {
+ $this->addFieldMapping('pathauto')
+ ->issueGroup(t('DNM'));
+ }
+ }
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.install.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.install.inc
index 42798763..3eb886b2 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.install.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/beer.install.inc
@@ -47,10 +47,7 @@ function migrate_example_beer_uninstall() {
}
function migrate_example_beer_disable() {
- Migration::deregisterMigration('BeerTerm');
- Migration::deregisterMigration('BeerUser');
- Migration::deregisterMigration('BeerNode');
- Migration::deregisterMigration('BeerComment');
+ MigrateGroup::deregister('beer');
}
function migrate_example_beer_schema_node() {
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.info b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.info
index 0b0f064a..5bf403e2 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.info
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.info
@@ -1,6 +1,6 @@
name = "Migrate Example"
description = "Example migration data."
-package = "Development"
+package = "Migration"
core = 7.x
dependencies[] = taxonomy
dependencies[] = image
@@ -12,16 +12,15 @@ dependencies[] = number
;node_reference is useful but not required
;dependencies[] = node_reference
-files[] = migrate_example.module
files[] = beer.inc
files[] = wine.inc
; For testing table_copy plugin. Since is infrequently used, we comment it out.
; files[] = example.table_copy.inc
-; Information added by drupal.org packaging script on 2012-11-07
-version = "7.x-2.5"
+; Information added by Drupal.org packaging script on 2015-02-09
+version = "7.x-2.7"
core = "7.x"
project = "migrate"
-datestamp = "1352299007"
+datestamp = "1423521491"
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.install b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.install
index fe93b684..073f1513 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.install
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.install
@@ -34,6 +34,7 @@ function migrate_example_install() {
);
$example_format = (object) $example_format;
filter_format_save($example_format);
+ migrate_static_registration();
}
function migrate_example_uninstall() {
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.migrate.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.migrate.inc
index 6d509eac..8e24d23c 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.migrate.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.migrate.inc
@@ -9,39 +9,238 @@
/*
* You must implement hook_migrate_api(), setting the API level to 2, if you are
- * implementing any migration classes. As of Migrate 2.5, you should also
- * register your migration and handler classes explicitly here - the former
- * method of letting them get automatically registered on a cache clear will
- * break in certain environments (see http://drupal.org/node/1778952).
+ * implementing any migration classes. If your migration application is static -
+ * that is, you know at implementation time exactly what migrations must be
+ * instantiated - then you should register your migrations here. If your
+ * application is more dynamic (for example, if selections in the UI determine
+ * exactly what migrations need to be instantiated), then you would register
+ * your migrations using registerMigration() - see migrate_example_baseball for
+ * more information.
*/
function migrate_example_migrate_api() {
+ // Usually field mappings are established by code in the migration constructor -
+ // a call to addFieldMapping(). They may also be passed as arguments when
+ // registering a migration - in this case, they are stored in the database
+ // and override any mappings for the same field in the code. To do this,
+ // construct the field mapping object and configure it similarly to when
+ // you call addFieldMapping, and pass your mappings as an array below.
+ $translate_mapping = new MigrateFieldMapping('translate', NULL);
+ $translate_mapping->defaultValue(0);
+ $ignore_mapping = new MigrateFieldMapping('migrate_example_beer_styles:ignore_case', NULL);
+ $ignore_mapping->defaultValue(1);
+
$api = array(
+ // Required - tells the Migrate module that you are implementing version 2
+ // of the Migrate API.
'api' => 2,
+ // Migrations can be organized into groups. The key used here will be the
+ // machine name of the group, which can be used in Drush:
+ // drush migrate-import --group=wine
+ // The title is a required argument which is displayed for the group in the
+ // UI. You may also have additional arguments for any other data which is
+ // common to all migrations in the group.
+ 'groups' => array(
+ 'beer' => array(
+ 'title' => t('Beer Imports'),
+ ),
+ 'wine' => array(
+ 'title' => t('Wine Imports'),
+ ),
+ ),
+
+ // Here we register the individual migrations. The keys (BeerTerm, BeerUser,
+ // etc.) are the machine names of the migrations, and the class_name
+ // argument is required. The group_name is optional (defaulting to 'default')
+ // but specifying it is a best practice.
'migrations' => array(
- 'BeerTerm' => array('class_name' => 'BeerTermMigration'),
- 'BeerUser' => array('class_name' => 'BeerUserMigration'),
- 'BeerNode' => array('class_name' => 'BeerNodeMigration'),
- 'BeerComment' => array('class_name' => 'BeerCommentMigration'),
- 'WinePrep' => array('class_name' => 'WinePrepMigration'),
- 'WineVariety' => array('class_name' => 'WineVarietyMigration'),
- 'WineRegion' => array('class_name' => 'WineRegionMigration'),
- 'WineBestWith' => array('class_name' => 'WineBestWithMigration'),
- 'WineFileCopy' => array('class_name' => 'WineFileCopyMigration'),
- 'WineFileBlob' => array('class_name' => 'WineFileBlobMigration'),
- 'WineRole' => array('class_name' => 'WineRoleMigration'),
- 'WineUser' => array('class_name' => 'WineUserMigration'),
- 'WineProducer' => array('class_name' => 'WineProducerMigration'),
- 'WineProducerXML' => array('class_name' => 'WineProducerXMLMigration'),
- 'WineProducerMultiXML' => array('class_name' => 'WineProducerMultiXMLMigration'),
- 'WineProducerXMLPull' => array('class_name' => 'WineProducerXMLPullMigration'),
- 'WineWine' => array('class_name' => 'WineWineMigration'),
- 'WineComment' => array('class_name' => 'WineCommentMigration'),
- 'WineTable' => array('class_name' => 'WineTableMigration'),
- 'WineFinish' => array('class_name' => 'WineFinishMigration'),
- 'WineUpdates' => array('class_name' => 'WineUpdatesMigration'),
- 'WineCommentUpdates' => array('class_name' => 'WineCommentUpdatesMigration'),
- 'WineVarietyUpdates' => array('class_name' => 'WineVarietyUpdatesMigration'),
- 'WineUserUpdates' => array('class_name' => 'WineUserUpdatesMigration'),
+ 'BeerTerm' => array(
+ 'class_name' => 'BeerTermMigration',
+ 'group_name' => 'beer',
+ ),
+ 'BeerUser' => array(
+ 'class_name' => 'BeerUserMigration',
+ 'group_name' => 'beer',
+ ),
+ 'BeerNode' => array(
+ 'class_name' => 'BeerNodeMigration',
+ 'group_name' => 'beer',
+ // You may optionally declare dependencies for your migration - other
+ // migrations which should run first. In this case, terms assigned to our
+ // nodes and the authors of the nodes should be migrated before the nodes
+ // themselves.
+ 'dependencies' => array(
+ 'BeerTerm',
+ 'BeerUser',
+ ),
+ // Here is where we add field mappings which may override those
+ // specified in the group constructor.
+ 'field_mappings' => array(
+ $translate_mapping,
+ $ignore_mapping,
+ ),
+ ),
+ 'BeerComment' => array(
+ 'class_name' => 'BeerCommentMigration',
+ 'group_name' => 'beer',
+ 'dependencies' => array(
+ 'BeerUser',
+ 'BeerNode',
+ ),
+ ),
+ 'WinePrep' => array(
+ 'class_name' => 'WinePrepMigration',
+ 'group_name' => 'wine',
+ ),
+ 'WineVariety' => array(
+ 'class_name' => 'WineVarietyMigration',
+ 'group_name' => 'wine',
+ ),
+ 'WineRegion' => array(
+ 'class_name' => 'WineRegionMigration',
+ 'group_name' => 'wine',
+ ),
+ 'WineBestWith' => array(
+ 'class_name' => 'WineBestWithMigration',
+ 'group_name' => 'wine',
+ ),
+ 'WineFileCopy' => array(
+ 'class_name' => 'WineFileCopyMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WinePrep'),
+ ),
+ 'WineFileBlob' => array(
+ 'class_name' => 'WineFileBlobMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WinePrep'),
+ ),
+ 'WineRole' => array(
+ 'class_name' => 'WineRoleMigration',
+ 'group_name' => 'wine',
+ // TIP: Regular dependencies, besides enforcing (in the absence of
+ // --force) the run order of migrations, affect the sorting of
+ // migrations on display. You can use soft dependencies to affect just
+ // the display order when the migrations aren't technically required to
+ // run in a certain order. In this case, we want the role migration to
+ // appear after the file migrations.
+ 'soft_dependencies' => array('WineFileCopy'),
+ ),
+ 'WineUser' => array(
+ 'class_name' => 'WineUserMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineFileCopy',
+ 'WineRole',
+ ),
+ ),
+ 'WineProducer' => array(
+ 'class_name' => 'WineProducerMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineProducerXML' => array(
+ 'class_name' => 'WineProducerXMLMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineProducerNamespaceXML' => array(
+ 'class_name' => 'WineProducerNamespaceXMLMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineProducerMultiXML' => array(
+ 'class_name' => 'WineProducerMultiXMLMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineProducerMultiNamespaceXML' => array(
+ 'class_name' => 'WineProducerMultiNamespaceXMLMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineProducerXMLPull' => array(
+ 'class_name' => 'WineProducerXMLPullMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineProducerNamespaceXMLPull' => array(
+ 'class_name' => 'WineProducerNamespaceXMLPullMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineUser',
+ ),
+ ),
+ 'WineWine' => array(
+ 'class_name' => 'WineWineMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineRegion',
+ 'WineVariety',
+ 'WineBestWith',
+ 'WineUser',
+ 'WineProducer',
+ ),
+ ),
+ 'WineComment' => array(
+ 'class_name' => 'WineCommentMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array(
+ 'WineUser',
+ 'WineWine',
+ ),
+ ),
+ 'WineTable' => array(
+ 'class_name' => 'WineTableMigration',
+ 'group_name' => 'wine',
+ 'soft_dependencies' => array('WineComment'),
+ ),
+ 'WineFinish' => array(
+ 'class_name' => 'WineFinishMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WineComment'),
+ ),
+ 'WineUpdates' => array(
+ 'class_name' => 'WineUpdatesMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WineWine'),
+ 'soft_dependencies' => array('WineFinish'),
+ ),
+ 'WineCommentUpdates' => array(
+ 'class_name' => 'WineCommentUpdatesMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WineComment'),
+ 'soft_dependencies' => array('WineUpdates'),
+ ),
+ 'WineVarietyUpdates' => array(
+ 'class_name' => 'WineVarietyUpdatesMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WineVariety'),
+ 'soft_dependencies' => array('WineUpdates'),
+ ),
+ 'WineUserUpdates' => array(
+ 'class_name' => 'WineUserUpdatesMigration',
+ 'group_name' => 'wine',
+ 'dependencies' => array('WineUser'),
+ 'soft_dependencies' => array('WineUpdates'),
+ ),
),
);
return $api;
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.module b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.module
index bae5c652..1dda30d5 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.module
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example.module
@@ -2,13 +2,13 @@
/**
* @file
- * THIS SPACE INTENTIONALLY LEFT BLANK.
+ * THIS FILE INTENTIONALLY LEFT BLANK.
*
* Yes, there is no code in the .module file. Migrate operates almost entirely
* through classes, and by adding any files containing class definitions to the
* .info file, those files are automatically included only when the classes they
* contain are referenced. The one non-class piece you need to implement is
- * hook_migrate_api(), but because .migrate.inc is registered using hook_hook_info
- * by defining your implementation of that hook in mymodule.migrate.inc, it is
- * automatically invoked only when needed.
+ * hook_migrate_api(), but because .migrate.inc is registered using
+ * hook_hook_info, by defining your implementation of that hook in
+ * example.migrate.inc, it is automatically invoked only when needed.
*/
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example_oracle/migrate_example_oracle.info b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example_oracle/migrate_example_oracle.info
index dcbb949b..6c4c0e8a 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example_oracle/migrate_example_oracle.info
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/migrate_example_oracle/migrate_example_oracle.info
@@ -8,12 +8,12 @@ features[field][] = "node-migrate_example_oracle-field_mainimage"
features[node][] = "migrate_example_oracle"
files[] = "migrate_example_oracle.migrate.inc"
name = "Migrate example - Oracle"
-package = "Migrate Examples"
+package = "Migration"
project = "migrate_example_oracle"
-; Information added by drupal.org packaging script on 2012-11-07
-version = "7.x-2.5"
+; Information added by Drupal.org packaging script on 2015-02-09
+version = "7.x-2.7"
core = "7.x"
project = "migrate"
-datestamp = "1352299007"
+datestamp = "1423521491"
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.inc
index 8502b89a..9824ce7d 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.inc
@@ -13,24 +13,20 @@
/**
* Abstract intermediate class holding common settings.
*/
-abstract class AdvancedExampleMigration extends DynamicMigration {
+abstract class AdvancedExampleMigration extends Migration {
+ /**
+ * Text format object for our migrate_example format.
+ * @var
+ */
public $basicFormat;
- public function __construct() {
- // TIP: Migrations can be organized into groups. In this case, all the migrations
- // derived from AdvancedExampleMigration will be part of the 'wine' group.
- // This enables us to easily run just the wine example migrations:
- // drush migrate-import --group=wine
- // The second argument to MigrateGroup::getInstance is an array of groups
- // which should come before this when viewing migration statuses, or running
- // migration operations using --all. Since the beer migrations in this module
- // did not specify a group, it is in the 'default' group, so this constructor
- // indicates that the wine migrations come after the beer migrations.
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->team = array(
new MigrateTeamMember('Jack Kramer', 'jkramer@example.com', t('Taster')),
- new MigrateTeamMember('Linda Madison', 'lmadison@example.com', t('Winemaker')),
+ new MigrateTeamMember('Linda Madison', 'lmadison@example.com',
+ t('Winemaker')),
);
$this->issuePattern = 'http://drupal.org/node/:id:';
@@ -39,34 +35,33 @@ abstract class AdvancedExampleMigration extends DynamicMigration {
// We can do shared field mappings in the common class
if (module_exists('path')) {
- $this->addFieldMapping('path')
+ $this->addFieldMapping('path')
+ ->issueGroup(t('DNM'));
+ if (module_exists('pathauto')) {
+ $this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
- $this->addFieldMapping('pathauto')
- ->issueGroup(t('DNM'));
- }
}
+ }
}
}
/**
* TIP: While usually you'll create true migrations - processes that copy data
- * from some source into Drupal - you can also define processing steps for either
- * the import or rollback stages that take other actions. In this case, we want
- * to disable auto_nodetitle while the migration steps run. We'll re-enable it
- * over in WineFinishMigration.
+ * from some source into Drupal - you can also define processing steps for
+ * either the import or rollback stages that take other actions. In this case,
+ * we want to disable auto_nodetitle while the migration steps run. We'll
+ * re-enable it over in WineFinishMigration.
*/
class WinePrepMigration extends MigrationBase {
// Track whether the auto_nodetitle was originally enabled so we know whether
// to re-enable it. This is public so WineFinishMigration can reference it.
public static $wasEnabled = FALSE;
- public function __construct() {
- // Because we're derived directly from migrationBase rather than AdvancedExampleMigration,
- // we must specify the group again here.
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('If auto_nodetitle is present, disable it for the duration');
}
+
// Define isComplete(), returning a boolean, to indicate whether dependent
// migrations may proceed
public function isComplete() {
@@ -78,6 +73,7 @@ class WinePrepMigration extends MigrationBase {
return TRUE;
}
}
+
// Implement any action you want to occur during an import process in an
// import() method (alternatively, if you have an action which you want to
// run during rollbacks, define a rollback() method).
@@ -91,17 +87,37 @@ class WinePrepMigration extends MigrationBase {
self::$wasEnabled = FALSE;
self::displayMessage(t('Auto_nodetitle is already disabled'), 'success');
}
- // Must return one of the MigrationBase RESULT constants
+ // import() must return one of the MigrationBase RESULT constants.
return MigrationBase::RESULT_COMPLETED;
}
}
-// The term migrations are very similar - implement the commonalities here
+// The term migrations are very similar - implement the commonalities here (yes,
+// two layers of abstraction).
abstract class WineTermMigration extends AdvancedExampleMigration {
- public function __construct($type, $vocabulary_name, $description) {
- parent::__construct();
+ // The type, vocabulary machine name, and description are the only
+ // differences among the incoming vocabularies, so pass them through the
+ // constructor - you'll see below that the individual term migrations classes
+ // thus become very simple.
+ public function __construct($arguments, $type, $vocabulary_name,
+ $description) {
+ parent::__construct($arguments);
$this->description = $description;
+
+ // Best practice as of Migrate 2.6 is to specify dependencies in
+ // hook_migrate_api. However, in this case, since we have a common class
+ // for a few different migrations, we'll specify this dependency here
+ // rather than repeatedly in hook_migrate_api().
$this->dependencies = array('WinePrep');
+
+ $query = db_select('migrate_example_wine_categories', 'wc')
+ ->fields('wc', array('categoryid', 'name', 'details',
+ 'category_parent', 'ordering'))
+ ->condition('type', $type)
+ // This sort assures that parents are saved before children.
+ ->orderBy('category_parent', 'ASC');
+ $this->source = new MigrateSourceSQL($query);
+ $this->destination = new MigrateDestinationTerm($vocabulary_name);
$this->map = new MigrateSQLMap($this->machineName,
array(
'categoryid' => array('type' => 'int',
@@ -112,14 +128,6 @@ abstract class WineTermMigration extends AdvancedExampleMigration {
MigrateDestinationTerm::getKeySchema()
);
- $query = db_select('migrate_example_wine_categories', 'wc')
- ->fields('wc', array('categoryid', 'name', 'details', 'category_parent', 'ordering'))
- ->condition('type', $type)
- // This sort assures that parents are saved before children.
- ->orderBy('category_parent', 'ASC');
- $this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationTerm($vocabulary_name);
-
// Mapped fields
$this->addFieldMapping('name', 'name');
$this->addFieldMapping('description', 'details');
@@ -138,134 +146,164 @@ abstract class WineTermMigration extends AdvancedExampleMigration {
}
class WineVarietyMigration extends WineTermMigration {
- public function __construct() {
- parent::__construct('variety', 'migrate_example_wine_varieties',
+ public function __construct($arguments) {
+ parent::__construct($arguments, 'variety', 'migrate_example_wine_varieties',
t('Migrate varieties from the source database to taxonomy terms'));
}
}
class WineRegionMigration extends WineTermMigration {
- public function __construct() {
- parent::__construct('region', 'migrate_example_wine_regions',
+ public function __construct($arguments) {
+ parent::__construct($arguments, 'region', 'migrate_example_wine_regions',
t('Migrate regions from the source database to taxonomy terms'));
}
}
class WineBestWithMigration extends WineTermMigration {
- public function __construct() {
- parent::__construct('best_with', 'migrate_example_wine_best_with',
+ public function __construct($arguments) {
+ parent::__construct($arguments, 'best_with', 'migrate_example_wine_best_with',
t('Migrate "Best With" from the source database to taxonomy terms'));
}
}
/**
- * TIP: Files can be migrated directly by themselves, by using the MigrateDestinationFile
- * class. This will copy the files themselves from the source, and set up the
- * Drupal file tables appropriately.
+ * TIP: Files can be migrated directly by themselves, by using the
+ * MigrateDestinationFile class. This will copy the files themselves from the
+ * source, and set up the Drupal file tables appropriately. Referencing them
+ * in, say, a node field later is then simple.
*/
class WineFileCopyMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Profile images');
- $this->dependencies = array('WinePrep');
- $this->map = new MigrateSQLMap($this->machineName,
- array('imageid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'Image ID.'
- )
- ),
- MigrateDestinationFile::getKeySchema()
- );
+
$query = db_select('migrate_example_wine_files', 'wf')
->fields('wf', array('imageid', 'url'))
->isNull('wineid');
$this->source = new MigrateSourceSQL($query);
-
$this->destination = new MigrateDestinationFile();
+ $this->map = new MigrateSQLMap($this->machineName,
+ array('imageid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Image ID.'
+ )
+ ),
+ MigrateDestinationFile::getKeySchema()
+ );
// In the simplest case, just map the incoming URL to 'value'.
$this->addFieldMapping('value', 'url');
- $this->addUnmigratedDestinations(array('fid', 'uid', 'timestamp',
- 'destination_dir', 'destination_file', 'source_dir', 'preserve_files',
- 'file_replace'));
- $this->removeFieldMapping('path');
+ $this->addUnmigratedDestinations(array(
+ 'destination_dir',
+ 'destination_file',
+ 'fid',
+ 'file_replace',
+ 'preserve_files',
+ 'source_dir',
+ 'timestamp',
+ 'uid',
+ 'urlencode',
+ ));
+
$this->removeFieldMapping('pathauto');
}
}
/**
* Migration class to test importing from a BLOB column into a file entity.
- * Typically, one would use this OR import into a File Field.
+ *
* @see MigrateExampleOracleNode()
*/
class WineFileBlobMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Example migration from BLOB column into files.');
- $this->dependencies = array('WinePrep');
- $this->map = new MigrateSQLMap($this->machineName,
- array('imageid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'Image ID.'
- )
- ),
- MigrateDestinationFile::getKeySchema()
- );
+
$query = db_select('migrate_example_wine_blobs', 'wf')
->fields('wf', array('imageid', 'imageblob'));
$this->source = new MigrateSourceSQL($query);
// Note that the WineFileCopyMigration example let the second argument,
- // the file_class, to default to MigrateFileUri, indicating that the
+ // the file_class, default to MigrateFileUri, indicating that the
// 'value' we're passing was a URI. In this case, we're passing a blob, so
// tell the destination to expect it.
$this->destination = new MigrateDestinationFile('file', 'MigrateFileBlob');
+ $this->map = new MigrateSQLMap($this->machineName,
+ array('imageid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Image ID.'
+ )
+ ),
+ MigrateDestinationFile::getKeySchema()
+ );
+
// Basic fields
- $this->addFieldMapping('uid')
- ->defaultValue(1);
- // The destination filename must be specified for blobs
- $this->addFieldMapping('destination_file')
- ->defaultValue('druplicon.png');
$this->addFieldMapping('value', 'imageblob')
->description('An image blob in the DB');
- // Unmapped destination fields
- $this->addUnmigratedDestinations(array('fid', 'timestamp',
- 'destination_dir', 'file_replace', 'preserve_files'));
+ // The destination filename must be specified for blobs
+ $this->addFieldMapping('destination_file')
+ ->defaultValue('druplicon.png');
+
+ $this->addFieldMapping('uid')
+ ->defaultValue(1);
+
+ // Unmapped destination fields
+ $this->addUnmigratedDestinations(array(
+ 'destination_dir',
+ 'fid',
+ 'file_replace',
+ 'preserve_files',
+ 'timestamp',
+ ));
- // Our base class mapped these since most migrations use them, but not this
- // one, so remove them
- $this->removeFieldMapping('path');
$this->removeFieldMapping('pathauto');
}
}
class WineRoleMigration extends XMLMigration {
- public function __construct() {
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('XML feed (multi items) of roles (positions)');
- // TIP: Regular dependencies, besides enforcing (in the absence of --force)
- // the run order of migrations, affect the sorting of migrations on display.
- // You can use soft dependencies to affect just the display order when the
- // migrations aren't technically required to run in a certain order. In this
- // case, we want the role migration to appear after the file migration.
- $this->softDependencies = array('WineFileCopy');
-
- // There isn't a consistent way to automatically identify appropriate "fields"
- // from an XML feed, so we pass an explicit list of source fields
+ // There isn't a consistent way to automatically identify appropriate
+ // "fields" from an XML feed, so we pass an explicit list of source fields
$fields = array(
'name' => t('Position name'),
);
- // The source ID here is the one retrieved from each data item in the XML file, and
- // used to identify specific items
+ // IMPORTANT: Do not try this at home! We have included importable files
+ // with the migrate_example module so it can be very simply installed and
+ // run, but you should never include any data you want to keep private
+ // (especially user data like email addresses, phone numbers, etc.) in the
+ // module directory. Your source data should be outside of the webroot, and
+ // should not be anywhere where it may get committed into a revision control
+ // system.
+
+ // This can also be an URL instead of a local file path.
+ $xml_folder = DRUPAL_ROOT . '/' .
+ drupal_get_path('module', 'migrate_example') . '/xml/';
+ $items_url = $xml_folder . 'positions.xml';
+ // This is the xpath identifying the items to be migrated, relative to the
+ // document.
+ $item_xpath = '/positions/position';
+ // This is the xpath relative to the individual items - thus the full xpath
+ // of an ID will be /positions/position/sourceid.
+ $item_ID_xpath = 'sourceid';
+
+ $items_class = new MigrateItemsXML($items_url, $item_xpath, $item_ID_xpath);
+ $this->source = new MigrateSourceMultiItems($items_class, $fields);
+
+ $this->destination = new MigrateDestinationRole();
+
+ // The source ID here is the one retrieved from each data item in the XML
+ // file, and used to identify specific items
$this->map = new MigrateSQLMap($this->machineName,
array(
'sourceid' => array(
@@ -277,62 +315,43 @@ class WineRoleMigration extends XMLMigration {
MigrateDestinationRole::getKeySchema()
);
- // IMPORTANT: Do not try this at home! We have included importable files
- // with the migrate_example module so it can be very simply installed and
- // run, but you should never include any data you want to keep private
- // (especially user data like email addresses, phone numbers, etc.) in the
- // module directory. Your source data should be outside of the webroot, and
- // should not be anywhere where it may get committed into a revision control
- // system.
-
- // This can also be an URL instead of a file path.
- $xml_folder = DRUPAL_ROOT . '/' . drupal_get_path('module', 'migrate_example') . '/xml/';
- $items_url = $xml_folder . 'positions.xml';
- $item_xpath = '/positions/position'; // relative to document
- $item_ID_xpath = 'sourceid'; // relative to item_xpath and gets assembled
- // into full path /producers/producer/sourceid
-
- $items_class = new MigrateItemsXML($items_url, $item_xpath, $item_ID_xpath);
- $this->source = new MigrateSourceMultiItems($items_class, $fields);
-
- $this->destination = new MigrateDestinationRole();
-
$this->addFieldMapping('name', 'name')
->xpath('name');
+
$this->addUnmigratedDestinations(array('weight'));
}
}
class WineUserMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Wine Drinkers of the world');
- $this->dependencies = array('WinePrep', 'WineFileCopy', 'WineRole');
- $this->map = new MigrateSQLMap($this->machineName,
- array('accountid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'Account ID.'
- )
- ),
- MigrateDestinationUser::getKeySchema()
- );
+
$query = db_select('migrate_example_wine_account', 'wa')
->fields('wa', array('accountid', 'status', 'posted', 'name',
'password', 'mail', 'last_access', 'last_login',
'original_mail', 'sig', 'sex', 'imageid', 'positions'));
$this->source = new MigrateSourceSQL($query);
$this->destination = new MigrateDestinationUser();
+ $this->map = new MigrateSQLMap($this->machineName,
+ array('accountid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Account ID.'
+ )
+ ),
+ MigrateDestinationUser::getKeySchema()
+ );
// Mapped fields
$this->addSimpleMappings(array('name', 'status', 'mail'));
- $this->addFieldMapping('created', 'posted')
- ->description('See prepare method');
- $this->addFieldMapping('access', 'last_access')
- ->description('See prepare method');
- $this->addFieldMapping('login', 'last_login')
- ->description('See prepare method');
+ // Note that these date/time values are coming in as ISO strings, but
+ // Drupal uses UNIX timestamps. The user destination automatically
+ // translates them as necessary.
+ $this->addFieldMapping('created', 'posted');
+ $this->addFieldMapping('access', 'last_access');
+ $this->addFieldMapping('login', 'last_login');
$this->addFieldMapping('pass', 'password');
$this->addFieldMapping('roles', 'positions')
->separator(',')
@@ -349,22 +368,24 @@ class WineUserMigration extends AdvancedExampleMigration {
// Unmapped source fields
// Unmapped destination fields
- $this->addUnmigratedDestinations(array('theme', 'timezone', 'language',
- 'is_new', 'field_migrate_example_favbeers', 'role_names', 'data'));
+ $this->addUnmigratedDestinations(array(
+ 'data',
+ 'is_new',
+ 'language',
+ 'role_names',
+ 'theme',
+ 'timezone',
+ ));
}
public function prepare(stdClass $account, stdClass $row) {
- // Source dates are in ISO format.
- // Because the mappings above have been applied, $account->created contains
- // the date/time string now - we could also pass $row->posted here.
- $account->created = strtotime($account->created);
- $account->access = strtotime($account->access);
- $account->login = strtotime($account->login);
-
// Gender data comes in as M/F, needs to be saved as Male=0/Female=1
// TIP: Note that the Migration prepare method is called after all other
// prepare handlers. Most notably, the field handlers have had their way
- // and created field arrays, so we have to save in the same format.
+ // and created field arrays, so we have to save in the same format. In this
+ // case, best practice would be to use a callback instead (see below), we're
+ // just demonstrating here what the $account object looks like at this
+ // stage.
switch ($row->sex) {
case 'm':
case 'M':
@@ -382,10 +403,9 @@ class WineUserMigration extends AdvancedExampleMigration {
}
class WineProducerMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Wine producers of the world');
- $this->dependencies = array('WineRegion', 'WineUser');
$this->map = new MigrateSQLMap($this->machineName,
array(
@@ -400,10 +420,11 @@ class WineProducerMigration extends AdvancedExampleMigration {
);
$query = db_select('migrate_example_wine_producer', 'p')
- ->fields('p', array('producerid', 'name', 'body', 'excerpt', 'accountid'));
+ ->fields('p', array('producerid', 'name', 'body', 'excerpt',
+ 'accountid'));
// Region term is singletons, handled straighforwardly
$query->leftJoin('migrate_example_wine_category_producer', 'reg',
- "p.producerid = reg.producerid");
+ 'p.producerid = reg.producerid');
$query->addField('reg', 'categoryid', 'region');
$this->source = new MigrateSourceSQL($query);
@@ -427,12 +448,27 @@ class WineProducerMigration extends AdvancedExampleMigration {
// No unmapped source fields
// Unmapped destination fields
- $this->addUnmigratedDestinations(array('is_new', 'created', 'changed',
- 'status', 'promote', 'revision', 'language', 'revision_uid', 'log', 'tnid',
- 'body:format', 'body:language', 'migrate_example_wine_regions:create_term',
- 'comment'));
+ $this->addUnmigratedDestinations(array(
+ 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'tnid',
+ 'translate',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ ));
+
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
}
@@ -446,13 +482,12 @@ class WineProducerMigration extends AdvancedExampleMigration {
* from XMLMigration instead of Migration.
*/
class WineProducerXMLMigration extends XMLMigration {
- public function __construct() {
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('XML feed of wine producers of the world');
- $this->dependencies = array('WineRegion', 'WineUser');
- // There isn't a consistent way to automatically identify appropriate "fields"
- // from an XML feed, so we pass an explicit list of source fields
+ // There isn't a consistent way to automatically identify appropriate
+ // "fields" from an XML feed, so we pass an explicit list of source fields.
$fields = array(
'name' => t('Producer name'),
'description' => t('Description of producer'),
@@ -460,6 +495,32 @@ class WineProducerXMLMigration extends XMLMigration {
'region' => t('Name of region'),
);
+ // IMPORTANT: Do not try this at home! We have included importable files
+ // with the migrate_example module so it can be very simply installed and
+ // run, but you should never include any data you want to keep private
+ // (especially user data like email addresses, phone numbers, etc.) in the
+ // module directory. Your source data should be outside of the webroot, and
+ // should not be anywhere where it may get committed into a revision control
+ // system.
+
+ // This can also be an URL instead of a file path.
+ $xml_folder = DRUPAL_ROOT . '/' .
+ drupal_get_path('module', 'migrate_example') . '/xml/';
+ $list_url = $xml_folder . 'index.xml';
+ // Each ID retrieved from the list URL will be plugged into :id in the
+ // item URL to fetch the specific objects.
+ $item_url = $xml_folder . ':id.xml';
+
+ // We use the MigrateSourceList class for any source where we obtain the
+ // list of IDs to process separately from the data for each item. The
+ // listing and item are represented by separate classes, so for example we
+ // could replace the XML listing with a file directory listing, or the XML
+ // item with a JSON item.
+ $this->source = new MigrateSourceList(new MigrateListXML($list_url),
+ new MigrateItemXML($item_url), $fields);
+
+ $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
// The source ID here is the one retrieved from the XML listing file, and
// used to identify the specific item's file
$this->map = new MigrateSQLMap($this->machineName,
@@ -473,35 +534,10 @@ class WineProducerXMLMigration extends XMLMigration {
MigrateDestinationNode::getKeySchema()
);
- // IMPORTANT: Do not try this at home! We have included importable files
- // with the migrate_example module so it can be very simply installed and
- // run, but you should never include any data you want to keep private
- // (especially user data like email addresses, phone numbers, etc.) in the
- // module directory. Your source data should be outside of the webroot, and
- // should not be anywhere where it may get committed into a revision control
- // system.
-
- // This can also be an URL instead of a file path.
- $xml_folder = DRUPAL_ROOT . '/' . drupal_get_path('module', 'migrate_example') . '/xml/';
- $list_url = $xml_folder . 'index.xml';
- // Each ID retrieved from the list URL will be plugged into :id in the
- // item URL to fetch the specific objects.
- $item_url = $xml_folder . ':id.xml';
-
- // We use the MigrateSourceList class for any source where we obtain the list
- // of IDs to process separately from the data for each item. The listing
- // and item are represented by separate classes, so for example we could
- // replace the XML listing with a file directory listing, or the XML item
- // with a JSON item.
- $this->source = new MigrateSourceList(new MigrateListXML($list_url),
- new MigrateItemXML($item_url), $fields);
-
- $this->destination = new MigrateDestinationNode('migrate_example_producer');
-
// TIP: Note that for XML sources, in addition to the source field passed to
// addFieldMapping (the name under which it will be saved in the data row
- // passed through the migration process) we specify the Xpath used to retrieve
- // the value from the XML.
+ // passed through the migration process) we specify the Xpath used to
+ // retrieve the value from the XML.
$this->addFieldMapping('title', 'name')
->xpath('/producer/name');
$this->addFieldMapping('uid', 'authorid')
@@ -513,21 +549,151 @@ class WineProducerXMLMigration extends XMLMigration {
$this->addFieldMapping('body', 'description')
->xpath('/producer/description');
- $this->addUnmigratedDestinations(array('revision_uid', 'created', 'changed',
- 'status', 'promote', 'sticky', 'revision', 'log', 'language', 'tnid',
- 'is_new', 'body:summary', 'body:format', 'body:language',
- 'migrate_example_wine_regions:source_type', 'migrate_example_wine_regions:create_term',
- 'comment'));
- if (module_exists('path')) {
+ $this->addUnmigratedDestinations(array(
+ 'body:summary', 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'sticky',
+ 'tnid',
+ 'translate',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
$this->addFieldMapping('path')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
+ if (isset($destination_fields['pathauto'])) {
$this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
}
}
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
+ }
+ }
+}
+
+/**
+ * TIP: An example of importing from an XML feed with namespaces.
+ * See the files in the xml directory - index2.xml contains a list of IDs
+ * to import, and .xml is the data for a given producer.
+ *
+ * Note that, if basing a migration on an XML source, you need to derive it
+ * from XMLMigration instead of Migration.
+ */
+class WineProducerNamespaceXMLMigration extends XMLMigration {
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description = t('Namespaced XML feed of wine producers of the world');
+
+ // There isn't a consistent way to automatically identify appropriate
+ // "fields" from an XML feed, so we pass an explicit list of source fields.
+ $fields = array(
+ 'pr:name' => t('Producer name'),
+ 'pr:description' => t('Description of producer'),
+ 'pr:authorid' => t('Numeric ID of the author'),
+ 'pr:region' => t('Name of region'),
+ );
+
+ // IMPORTANT: Do not try this at home! We have included importable files
+ // with the migrate_example module so it can be very simply installed and
+ // run, but you should never include any data you want to keep private
+ // (especially user data like email addresses, phone numbers, etc.) in the
+ // module directory. Your source data should be outside of the webroot, and
+ // should not be anywhere where it may get committed into a revision control
+ // system.
+
+ // This can also be an URL instead of a file path.
+ $xml_folder = DRUPAL_ROOT . '/' .
+ drupal_get_path('module', 'migrate_example') . '/xml/';
+ $list_url = $xml_folder . 'index2.xml';
+ // Each ID retrieved from the list URL will be plugged into :id in the
+ // item URL to fetch the specific objects.
+ $item_url = $xml_folder . ':id.xml';
+
+ // We use the MigrateSourceList class for any source where we obtain the
+ // list of IDs to process separately from the data for each item. The
+ // listing and item are represented by separate classes, so for example we
+ // could replace the XML listing with a file directory listing, or the XML
+ // item with a JSON item.
+ $list = new MigrateListXML($list_url, array('wn' => 'http://www.wine.org/wine'));
+ $item = new MigrateItemXML($item_url, array('pr' => 'http://www.wine.org/wine-producers'));
+ $this->source = new MigrateSourceList($list, $item, $fields);
+
+ $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+ // The source ID here is the one retrieved from the XML listing file, and
+ // used to identify the specific item's file
+ $this->map = new MigrateSQLMap($this->machineName,
+ array(
+ 'sourceid' => array(
+ 'type' => 'varchar',
+ 'length' => 4,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationNode::getKeySchema()
+ );
+
+ // TIP: Note that for XML sources, in addition to the source field passed to
+ // addFieldMapping (the name under which it will be saved in the data row
+ // passed through the migration process) we specify the Xpath used to
+ // retrieve the value from the XML.
+ $this->addFieldMapping('title', 'pr:name')
+ ->xpath('/pr:producer/pr:name');
+ $this->addFieldMapping('uid', 'pr:authorid')
+ ->xpath('/pr:producer/pr:authorid')
+ ->sourceMigration('WineUser')
+ ->defaultValue(1);
+ $this->addFieldMapping('migrate_example_wine_regions', 'pr:region')
+ ->xpath('/pr:producer/pr:region');
+ $this->addFieldMapping('body', 'pr:description')
+ ->xpath('/pr:producer/pr:description');
+
+ $this->addUnmigratedDestinations(array(
+ 'body:summary', 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'sticky',
+ 'tnid',
+ 'translate',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
+ $this->addFieldMapping('path')
+ ->issueGroup(t('DNM'));
+ if (isset($destination_fields['pathauto'])) {
+ $this->addFieldMapping('pathauto')
+ ->issueGroup(t('DNM'));
+ }
+ }
+ if (module_exists('statistics')) {
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
}
@@ -542,13 +708,13 @@ class WineProducerXMLMigration extends XMLMigration {
* from XMLMigration instead of Migration.
*/
class WineProducerMultiXMLMigration extends XMLMigration {
- public function __construct() {
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
- $this->description = t('XML feed (multi items) of wine producers of the world');
- $this->dependencies = array('WineRegion', 'WineUser');
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description =
+ t('XML feed (multi items) of wine producers of the world');
- // There isn't a consistent way to automatically identify appropriate "fields"
- // from an XML feed, so we pass an explicit list of source fields
+ // There isn't a consistent way to automatically identify appropriate
+ // "fields" from an XML feed, so we pass an explicit list of source fields.
$fields = array(
'name' => t('Producer name'),
'description' => t('Description of producer'),
@@ -556,19 +722,6 @@ class WineProducerMultiXMLMigration extends XMLMigration {
'region' => t('Name of region'),
);
- // The source ID here is the one retrieved from each data item in the XML file, and
- // used to identify specific items
- $this->map = new MigrateSQLMap($this->machineName,
- array(
- 'sourceid' => array(
- 'type' => 'varchar',
- 'length' => 4,
- 'not null' => TRUE,
- )
- ),
- MigrateDestinationNode::getKeySchema()
- );
-
// IMPORTANT: Do not try this at home! We have included importable files
// with the migrate_example module so it can be very simply installed and
// run, but you should never include any data you want to keep private
@@ -581,29 +734,44 @@ class WineProducerMultiXMLMigration extends XMLMigration {
$xml_folder = DRUPAL_ROOT . '/' . drupal_get_path('module', 'migrate_example') . '/xml/';
$items_url = $xml_folder . 'producers.xml';
- // We use the MigrateSourceMultiItems class for any source where we obtain the list
- // of IDs to process and the data for each item from the same file. Typically the data
- // for an item is not contained in a single line within the source file. Examples include
- // multiple items defined in a single xml file or a single json file where in both cases
- // the id is part of the item.
+ // We use the MigrateSourceMultiItems class for any source where we obtain
+ // the list of IDs to process and the data for each item from the same
+ // file. Examples include multiple items defined in a single xml file or a
+ // single json file where in both cases the id is part of the item.
- $item_xpath = '/producers/producer'; // relative to document
-
- $item_ID_xpath = 'sourceid'; // relative to item_xpath and gets assembled
- // into full path /producers/producer/sourceid
+ // This is the xpath identifying the items to be migrated, relative to the
+ // document.
+ $item_xpath = '/producers/producer';
+ // This is the xpath relative to the individual items - thus the full xpath
+ // of an ID will be /producers/producer/sourceid.
+ $item_ID_xpath = 'sourceid';
$items_class = new MigrateItemsXML($items_url, $item_xpath, $item_ID_xpath);
$this->source = new MigrateSourceMultiItems($items_class, $fields);
$this->destination = new MigrateDestinationNode('migrate_example_producer');
+ // The source ID here is the one retrieved from each data item in the XML
+ // file, and used to identify specific items
+ $this->map = new MigrateSQLMap($this->machineName,
+ array(
+ 'sourceid' => array(
+ 'type' => 'varchar',
+ 'length' => 4,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationNode::getKeySchema()
+ );
+
// TIP: Note that for XML sources, in addition to the source field passed to
// addFieldMapping (the name under which it will be saved in the data row
- // passed through the migration process) we specify the Xpath used to retrieve
- // the value from the XML.
- // TIP: Note that all xpaths for fields begin at the last element of the item
- // xpath since each item xml chunk is processed individually.
- // (ex. xpath=name is equivalent to a full xpath of /producers/producer/name)
+ // passed through the migration process) we specify the Xpath used to
+ // retrieve the value from the XML.
+ // TIP: Note that all xpaths for fields begin at the last element of the
+ // item xpath since each item xml chunk is processed individually.
+ // (ex. xpath=name is equivalent to a full xpath of
+ // /producers/producer/name).
$this->addFieldMapping('title', 'name')
->xpath('name');
$this->addFieldMapping('uid', 'authorid')
@@ -615,54 +783,64 @@ class WineProducerMultiXMLMigration extends XMLMigration {
$this->addFieldMapping('body', 'description')
->xpath('description');
- $this->addUnmigratedDestinations(array('revision_uid', 'created', 'changed',
- 'status', 'promote', 'sticky', 'revision', 'log', 'language', 'tnid',
- 'is_new', 'body:summary', 'body:format', 'body:language',
- 'migrate_example_wine_regions:source_type', 'migrate_example_wine_regions:create_term',
- 'comment'));
- if (module_exists('path')) {
+ $this->addUnmigratedDestinations(array(
+ 'body:summary', 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'sticky',
+ 'tnid',
+ 'translate',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
$this->addFieldMapping('path')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
+ if (isset($destination_fields['pathauto'])) {
$this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
}
}
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
}
/**
- * TIP: An alternative approach using MigrateSourceSQL. This uses a different
- * XML library, which advances element-by-element through the XML file rather
- * than reading in the whole file. This source will work better with large XML
- * files, but is slower for small files and has a more restrictive query lanaguage
- * for selecting the elements to process.
+ * TIP: An example of importing from an XML feed with namespaces, where both
+ * the id and the data to import are in the same file. The id is a part of
+ * the data. See the file in the xml directory - producers3.xml which contains
+ * all IDs and producer data for this example.
+ *
+ * Note that, if basing a migration on an XML source, you need to derive it
+ * from XMLMigration instead of Migration.
*/
-class WineProducerXMLPullMigration extends XMLMigration {
- public function __construct() {
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
- $this->description = t('XML feed (pull) of wine producers of the world');
- $this->dependencies = array('WineRegion', 'WineUser');
+class WineProducerMultiNamespaceXMLMigration extends XMLMigration {
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description =
+ t('Namespaced XML feed (multi items) of wine producers of the world');
+ // There isn't a consistent way to automatically identify appropriate
+ // "fields" from an XML feed, so we pass an explicit list of source fields.
$fields = array(
- 'name' => t('Producer name'),
- 'description' => t('Description of producer'),
- 'authorid' => t('Numeric ID of the author'),
- 'region' => t('Name of region'),
- );
-
- $this->map = new MigrateSQLMap($this->machineName,
- array(
- 'sourceid' => array(
- 'type' => 'varchar',
- 'length' => 4,
- 'not null' => TRUE,
- )
- ),
- MigrateDestinationNode::getKeySchema()
+ 'pr:name' => t('Producer name'),
+ 'pr:description' => t('Description of producer'),
+ 'pr:authorid' => t('Numeric ID of the author'),
+ 'pr:region' => t('Name of region'),
);
// IMPORTANT: Do not try this at home! We have included importable files
@@ -675,21 +853,153 @@ class WineProducerXMLPullMigration extends XMLMigration {
// This can also be an URL instead of a file path.
$xml_folder = DRUPAL_ROOT . '/' . drupal_get_path('module', 'migrate_example') . '/xml/';
+ $items_url = $xml_folder . 'producers3.xml';
+
+ // We use the MigrateSourceMultiItems class for any source where we obtain
+ // the list of IDs to process and the data for each item from the same
+ // file. Examples include multiple items defined in a single xml file or a
+ // single json file where in both cases the id is part of the item.
+
+ // This is the xpath identifying the items to be migrated, relative to the
+ // document.
+ $item_xpath = '/pr:producers/pr:producer';
+ // This is the xpath relative to the individual items - thus the full xpath
+ // of an ID will be /producers/producer/sourceid.
+ $item_ID_xpath = 'pr:sourceid';
+ // All XML namespaces used in the XML file need to be defined here too.
+ $namespaces = array('pr' => 'http://www.wine.org/wine-producers');
+
+ $items_class = new MigrateItemsXML($items_url, $item_xpath, $item_ID_xpath, $namespaces);
+ $this->source = new MigrateSourceMultiItems($items_class, $fields);
+
+ $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+ // The source ID here is the one retrieved from each data item in the XML
+ // file, and used to identify specific items
+ $this->map = new MigrateSQLMap($this->machineName,
+ array(
+ 'sourceid' => array(
+ 'type' => 'varchar',
+ 'length' => 4,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationNode::getKeySchema()
+ );
+
+ // TIP: Note that for XML sources, in addition to the source field passed to
+ // addFieldMapping (the name under which it will be saved in the data row
+ // passed through the migration process) we specify the Xpath used to
+ // retrieve the value from the XML.
+ // TIP: Note that all xpaths for fields begin at the last element of the
+ // item xpath since each item xml chunk is processed individually.
+ // (ex. xpath=name is equivalent to a full xpath of
+ // /producers/producer/name).
+ $this->addFieldMapping('title', 'pr:name')
+ ->xpath('pr:name');
+ $this->addFieldMapping('uid', 'pr:authorid')
+ ->xpath('pr:authorid')
+ ->sourceMigration('WineUser')
+ ->defaultValue(1);
+ $this->addFieldMapping('migrate_example_wine_regions', 'pr:region')
+ ->xpath('pr:region');
+ $this->addFieldMapping('body', 'pr:description')
+ ->xpath('pr:description');
+
+ $this->addUnmigratedDestinations(array(
+ 'body:summary', 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'sticky',
+ 'tnid',
+ 'translate',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
+ $this->addFieldMapping('path')
+ ->issueGroup(t('DNM'));
+ if (isset($destination_fields['pathauto'])) {
+ $this->addFieldMapping('pathauto')
+ ->issueGroup(t('DNM'));
+ }
+ }
+ if (module_exists('statistics')) {
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
+ }
+ }
+}
+
+/**
+ * TIP: An alternative approach using MigrateSourceSQL. This uses a different
+ * XML library, which advances element-by-element through the XML file rather
+ * than reading in the whole file. This source will work better with large XML
+ * files, but is slower for small files and has a more restrictive query
+ * language for selecting the elements to process.
+ */
+class WineProducerXMLPullMigration extends XMLMigration {
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description = t('XML feed (pull) of wine producers of the world');
+
+ $fields = array(
+ 'name' => t('Producer name'),
+ 'description' => t('Description of producer'),
+ 'authorid' => t('Numeric ID of the author'),
+ 'region' => t('Name of region'),
+ );
+
+ // IMPORTANT: Do not try this at home! We have included importable files
+ // with the migrate_example module so it can be very simply installed and
+ // run, but you should never include any data you want to keep private
+ // (especially user data like email addresses, phone numbers, etc.) in the
+ // module directory. Your source data should be outside of the webroot, and
+ // should not be anywhere where it may get committed into a revision control
+ // system.
+
+ // This can also be an URL instead of a local file path.
+ $xml_folder = DRUPAL_ROOT . '/' .
+ drupal_get_path('module', 'migrate_example') . '/xml/';
$items_url = $xml_folder . 'producers2.xml';
- // As with MigrateSourceMultiItems, this applies where there is not a separate
- // list of IDs to process - the source XML file is entirely self-contained.
- // For the ID path, and xpath for each component, we can use the full xpath
- // syntax as usual. However, the syntax to select the elements that correspond
- // to objects to import is more limited. It must be a fully-qualified path
- // to the element (i.e., /producers/producer rather than just //producer).
+ // As with MigrateSourceMultiItems, this applies where there is not a
+ // separate list of IDs to process - the source XML file is entirely
+ // self-contained. For the ID path, and xpath for each component, we can
+ // use the full xpath syntax as usual. However, the syntax to select the
+ // elements that correspond to objects to import is more limited. It must
+ // be a fully-qualified path to the element (i.e.,
+ // /producers/producer rather than just //producer).
$item_xpath = '/producers/producer'; // relative to document
$item_ID_xpath = 'sourceid'; // relative to item_xpath
- $this->source = new MigrateSourceXML($items_url, $item_xpath, $item_ID_xpath,
- $fields);
+ $this->source = new MigrateSourceXML($items_url, $item_xpath,
+ $item_ID_xpath, $fields);
$this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+ $this->map = new MigrateSQLMap($this->machineName,
+ array(
+ 'sourceid' => array(
+ 'type' => 'varchar',
+ 'length' => 4,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationNode::getKeySchema()
+ );
+
$this->addFieldMapping('title', 'name')
->xpath('name');
$this->addFieldMapping('uid', 'authorid')
@@ -701,32 +1011,208 @@ class WineProducerXMLPullMigration extends XMLMigration {
$this->addFieldMapping('body', 'description')
->xpath('description');
- $this->addUnmigratedDestinations(array('revision_uid', 'created', 'changed',
- 'status', 'promote', 'sticky', 'revision', 'log', 'language', 'tnid',
- 'is_new', 'body:summary', 'body:format', 'body:language',
- 'migrate_example_wine_regions:source_type', 'migrate_example_wine_regions:create_term',
- 'comment'));
- if (module_exists('path')) {
+ $this->addUnmigratedDestinations(array(
+ 'body:summary', 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'sticky',
+ 'tnid',
+ 'translate',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
$this->addFieldMapping('path')
->issueGroup(t('DNM'));
- if (module_exists('pathauto')) {
+ if (isset($destination_fields['pathauto'])) {
$this->addFieldMapping('pathauto')
->issueGroup(t('DNM'));
}
}
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
+ }
+ }
+}
+
+/**
+ * TIP: An alternative approach using MigrateSourceSQL. This uses a different
+ * XML library, which advances element-by-element through the XML file rather
+ * than reading in the whole file. This source will work better with large XML
+ * files, but is slower for small files and has a more restrictive query
+ * language for selecting the elements to process.
+ */
+class WineProducerNamespaceXMLPullMigration extends XMLMigration {
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description = t('XML feed with namespaces (pull) of wine producers of the world');
+
+ $fields = array(
+ 'pr:name' => t('Producer name'),
+ 'pr:description' => t('Description of producer'),
+ 'pr:authorid' => t('Numeric ID of the author'),
+ 'pr:region' => t('Name of region'),
+ );
+
+ // IMPORTANT: Do not try this at home! We have included importable files
+ // with the migrate_example module so it can be very simply installed and
+ // run, but you should never include any data you want to keep private
+ // (especially user data like email addresses, phone numbers, etc.) in the
+ // module directory. Your source data should be outside of the webroot, and
+ // should not be anywhere where it may get committed into a revision control
+ // system.
+
+ // This can also be an URL instead of a local file path.
+ $xml_folder = DRUPAL_ROOT . '/' .
+ drupal_get_path('module', 'migrate_example') . '/xml/';
+ $items_url = $xml_folder . 'producers4.xml';
+
+ // As with MigrateSourceMultiItems, this applies where there is not a
+ // separate list of IDs to process - the source XML file is entirely
+ // self-contained. For the ID path, and xpath for each component, we can
+ // use the full xpath syntax as usual. However, the syntax to select the
+ // elements that correspond to objects to import is more limited. It must
+ // be a fully-qualified path to the element (i.e.,
+ // /producers/producer rather than just //producer).
+ $item_xpath = '/pr:producers/pr:producer'; // relative to document
+ $item_ID_xpath = 'pr:sourceid'; // relative to item_xpath
+ $namespaces = array('pr' => 'http://www.wine.org/wine-producers');
+
+ $this->source = new MigrateSourceXML($items_url, $item_xpath,
+ $item_ID_xpath, $fields,
+ array(), $namespaces);
+
+ $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+ $this->map = new MigrateSQLMap($this->machineName,
+ array(
+ 'sourceid' => array(
+ 'type' => 'varchar',
+ 'length' => 4,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationNode::getKeySchema()
+ );
+
+ $this->addFieldMapping('title', 'pr:name')
+ ->xpath('pr:name');
+ $this->addFieldMapping('uid', 'pr:authorid')
+ ->xpath('pr:authorid')
+ ->sourceMigration('WineUser')
+ ->defaultValue(1);
+ $this->addFieldMapping('migrate_example_wine_regions', 'pr:region')
+ ->xpath('pr:region');
+ $this->addFieldMapping('body', 'pr:description')
+ ->xpath('pr:description');
+
+ $this->addUnmigratedDestinations(array(
+ 'body:summary', 'body:format', 'body:language',
+ 'changed',
+ 'comment',
+ 'created',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'sticky',
+ 'tnid',
+ 'translate',
+ ));
+
+ $destination_fields = $this->destination->fields();
+ if (isset($destination_fields['path'])) {
+ $this->addFieldMapping('path')
+ ->issueGroup(t('DNM'));
+ if (isset($destination_fields['pathauto'])) {
+ $this->addFieldMapping('pathauto')
+ ->issueGroup(t('DNM'));
+ }
+ }
+ if (module_exists('statistics')) {
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
}
// TODO: Add node_reference field pointing to producer
class WineWineMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Wines of the world');
- $this->dependencies = array('WineVariety', 'WineRegion',
- 'WineBestWith', 'WineUser', 'WineProducer');
+
+ $query = db_select('migrate_example_wine', 'w')
+ ->fields('w', array('wineid', 'name', 'body', 'excerpt',
+ 'accountid', 'posted', 'last_changed', 'variety', 'region',
+ 'rating'));
+ $query->leftJoin('migrate_example_wine_category_wine', 'cwbw',
+ "w.wineid = cwbw.wineid");
+ $query->leftJoin('migrate_example_wine_categories', 'bw',
+ "cwbw.categoryid = bw.categoryid AND bw.type = 'best_with'");
+ // Gives a single comma-separated list of related terms
+ $query->groupBy('w.wineid');
+ $query->addExpression('GROUP_CONCAT(bw.categoryid)', 'best_with');
+
+ $count_query = db_select('migrate_example_wine', 'w');
+ $count_query->addExpression('COUNT(wineid)', 'cnt');
+
+ // TIP: By passing an array of source fields to the MigrateSourceSQL
+ // constructor, we can modify the descriptions of source fields (which just
+ // default, for SQL migrations, to table_alias.column_name), as well as add
+ // additional fields (which may be populated in prepareRow()).
+ $source_fields = array(
+ 'wineid' => t('Wine ID in the old system'),
+ 'name' => t('The name of the wine'),
+ 'best_vintages' => t('What years were best for this wine?'),
+ 'url' => t('Image URLs attached to this wine; populated in prepareRow()'),
+ 'image_alt' =>
+ t('Image alt text attached to this wine; populated in prepareRow()'),
+ 'image_title' =>
+ t('Image titles attached to this wine; populated in prepareRow()'),
+ );
+
+ // TIP: By default, each time a migration is run, any previously unprocessed
+ // source items are imported (along with any previously-imported items
+ // marked for update). If the source data contains a timestamp that is set
+ // to the creation time of each new item, as well as set to the update time
+ // for any existing items that are updated, then you can have those updated
+ // items automatically reimported by setting the field as your highwater
+ // field.
+ $this->highwaterField = array(
+ 'name' => 'last_changed', // Column to be used as highwater mark
+ 'alias' => 'w', // Table alias containing that column
+ 'type' => 'int', // By default, highwater marks are assumed to
+ // be lexicographically sortable (e.g.,
+ // '2011-05-19 17:53:12'). To properly deal with
+ // integer highwater marks (such as UNIX
+ // timestamps), indicate so here.
+ );
+
+ // Note that it is important to process rows in the order of the highwater
+ // mark.
+ $query->orderBy('last_changed');
+
+ $this->source = new MigrateSourceSQL($query, $source_fields, $count_query);
+ $this->destination = new MigrateDestinationNode('migrate_example_wine');
// You can add a 'track_last_imported' option to the map, to record the
// timestamp of when each item was last imported in the map table.
@@ -741,57 +1227,10 @@ class WineWineMigration extends AdvancedExampleMigration {
)
),
MigrateDestinationNode::getKeySchema(),
- 'default',
+ 'default', // The SQL connection where the map/message tables are created.
array('track_last_imported' => TRUE)
);
- $query = db_select('migrate_example_wine', 'w')
- ->fields('w', array('wineid', 'name', 'body', 'excerpt', 'accountid',
- 'posted', 'last_changed', 'variety', 'region', 'rating'));
- $query->leftJoin('migrate_example_wine_category_wine', 'cwbw',
- "w.wineid = cwbw.wineid");
- $query->leftJoin('migrate_example_wine_categories', 'bw',
- "cwbw.categoryid = bw.categoryid AND bw.type = 'best_with'");
- // Gives a single comma-separated list of related terms
- $query->groupBy('w.wineid');
- $query->addExpression('GROUP_CONCAT(bw.categoryid)', 'best_with');
-
- $count_query = db_select('migrate_example_wine', 'w');
- $count_query->addExpression('COUNT(wineid)', 'cnt');
-
- // TIP: By passing an array of source fields to the MigrateSourceSQL constructor,
- // we can modify the descriptions of source fields (which just default, for
- // SQL migrations, to table_alias.column_name), as well as add additional fields
- // (which may be populated in prepareRow()).
- $source_fields = array(
- 'wineid' => t('Wine ID in the old system'),
- 'name' => t('The name of the wine'),
- 'best_vintages' => t('What years were best for this wine?'),
- 'url' => t('Image URLs attached to this wine; populated in prepareRow()'),
- 'image_alt' => t('Image alt text attached to this wine; populated in prepareRow()'),
- 'image_title' => t('Image titles attached to this wine; populated in prepareRow()'),
- );
-
- // TIP: By default, each time a migration is run, any previously unimported source items
- // are imported (along with any previously-imported items marked for update). If the
- // source data contains a timestamp that is set to the creation time of each new item,
- // as well as set to the update time for any existing items that are updated, then
- // you can have those updated items automatically reimported by setting the field as
- // your highwater field.
- $this->highwaterField = array(
- 'name' => 'last_changed', // Column to be used as highwater mark
- 'alias' => 'w', // Table alias containing that column
- 'type' => 'int', // By default, highwater marks are assumed to be lexicographically
- // sortable (e.g., '2011-05-19 17:53:12'). To properly
- // deal with integer highwater marks (such as UNIX
- // timestamps), indicate so here.
- );
- // Note that it is important to process rows in the order of the highwater mark
- $query->orderBy('last_changed');
-
- $this->source = new MigrateSourceSQL($query, $source_fields, $count_query);
- $this->destination = new MigrateDestinationNode('migrate_example_wine');
-
// Mapped fields
$this->addFieldMapping('title', 'name')
->description(t('Mapping wine name in source to node title'));
@@ -815,15 +1254,17 @@ class WineWineMigration extends AdvancedExampleMigration {
->sourceMigration('WineBestWith');
$this->addFieldMapping('migrate_example_wine_best_with:source_type')
->defaultValue('tid');
+
$this->addFieldMapping('field_migrate_example_wine_ratin', 'rating');
$this->addFieldMapping('field_migrate_example_top_vintag', 'best_vintages');
- // TIP: You can apply one or more functions to a source value using ->callbacks().
- // The function must take a single argument and return a value which is a
- // transformation of the argument. As this example shows, you can have multiple
- // callbacks, and they can either be straight functions or class methods. In
- // this case, our custom method prepends 'review: ' to the body, and then we
- // call a standard Drupal function to uppercase the whole body.
+ // TIP: You can apply one or more functions to a source value using
+ // ->callbacks(). The function must take a single argument and return a
+ // value which is a transformation of the argument. As this example shows,
+ // you can have multiple callbacks, and they can either be straight
+ // functions or class methods. In this case, our custom method prepends
+ // 'review: ' to the body, and then we call a standard Drupal function to
+ // uppercase the whole body.
$this->addFieldMapping('body', 'body')
->callbacks(array($this, 'addTitlePrefix'), 'drupal_strtoupper');
$this->addFieldMapping('body:summary', 'excerpt');
@@ -840,23 +1281,44 @@ class WineWineMigration extends AdvancedExampleMigration {
$this->addFieldMapping('sticky')
->defaultValue(0);
- // These are already UNIX timestamps, so just pass through
+
$this->addFieldMapping('created', 'posted');
$this->addFieldMapping('changed', 'last_changed');
- // No unmapped source fields
+ // Unmapped source fields
+ $this->addUnmigratedSources(array('last_changed'));
// Unmapped destination fields
- $this->addUnmigratedDestinations(array('revision_uid', 'status', 'promote',
- 'revision', 'log', 'language', 'tnid', 'is_new', 'body:format',
- 'body:language', 'migrate_example_wine_regions:create_term', 'comment',
- 'migrate_example_wine_varieties:create_term', 'migrate_example_wine_best_with:create_term',
- 'field_migrate_example_image:language', 'field_migrate_example_image:preserve_files',
- 'field_migrate_example_image:source_dir', 'field_migrate_example_image:destination_dir',
- 'field_migrate_example_image:destination_file', 'field_migrate_example_image:file_class',
+ $this->addUnmigratedDestinations(array(
+ 'body:format', 'body:language',
+ 'comment',
+ 'field_migrate_example_image:destination_dir',
+ 'field_migrate_example_image:destination_file',
+ 'field_migrate_example_image:file_class',
+ 'field_migrate_example_image:language',
+ 'field_migrate_example_image:preserve_files',
+ 'field_migrate_example_image:source_dir',
+ 'field_migrate_example_image:urlencode',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_best_with:create_term',
+ 'migrate_example_wine_best_with:ignore_case',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_varieties:create_term',
+ 'migrate_example_wine_varieties:ignore_case',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'tnid',
+ 'translate',
));
+
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
@@ -865,11 +1327,19 @@ class WineWineMigration extends AdvancedExampleMigration {
}
// TIP: Implement a prepareRow() method to manipulate the source row between
- // retrieval from the database and the automatic applicaton of mappings
+ // retrieval from the database and the automatic applicaton of mappings.
public function prepareRow($current_row) {
- // We used the MySQL GROUP_CONCAT function above to handle a multi-value source
- // field - more portably, we query the related table with multiple values here,
- // so the values can run through the mapping process
+ // Always start your prepareRow implementation with this clause. You need to
+ // be sure your parent classes have their chance at the row, and that if
+ // they return FALSE (indicating the row should be skipped) you pass that
+ // on.
+ if (parent::prepareRow($current_row) === FALSE) {
+ return FALSE;
+ }
+
+ // We used the MySQL GROUP_CONCAT function above to handle a multi-value
+ // source field - more portably, we query the related table with multiple
+ // values here, so the values can run through the mapping process.
$source_id = $current_row->wineid;
$result = db_select('migrate_example_wine_vintages', 'v')
->fields('v', array('vintage'))
@@ -895,6 +1365,7 @@ class WineWineMigration extends AdvancedExampleMigration {
$current_row->image_title[] = $row->image_title;
}
*/
+
// We could also have used this function to decide to skip a row, in cases
// where that couldn't easily be done through the original query. Simply
// return FALSE in such cases.
@@ -903,10 +1374,18 @@ class WineWineMigration extends AdvancedExampleMigration {
}
class WineCommentMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = 'Comments about wines';
- $this->dependencies = array('WineUser', 'WineWine');
+
+ $query = db_select('migrate_example_wine_comment', 'wc')
+ ->fields('wc', array('commentid', 'comment_parent', 'name', 'mail',
+ 'accountid', 'body', 'wineid', 'subject', 'commenthost',
+ 'userpage', 'posted', 'lastchanged'))
+ ->orderBy('comment_parent');
+ $this->source = new MigrateSourceSQL($query);
+ $this->destination =
+ new MigrateDestinationComment('comment_node_migrate_example_wine');
$this->map = new MigrateSQLMap($this->machineName,
array('commentid' => array(
'type' => 'int',
@@ -916,13 +1395,6 @@ class WineCommentMigration extends AdvancedExampleMigration {
),
MigrateDestinationComment::getKeySchema()
);
- $query = db_select('migrate_example_wine_comment', 'wc')
- ->fields('wc', array('commentid', 'comment_parent', 'name', 'mail',
- 'accountid', 'body', 'wineid', 'subject', 'commenthost', 'userpage',
- 'posted', 'lastchanged'))
- ->orderBy('comment_parent');
- $this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationComment('comment_node_migrate_example_wine');
// Mapped fields
$this->addSimpleMappings(array('name', 'subject', 'mail'));
@@ -945,35 +1417,40 @@ class WineCommentMigration extends AdvancedExampleMigration {
// No unmapped source fields
// Unmapped destination fields
- $this->addUnmigratedDestinations(array('thread', 'language',
- 'comment_body:format', 'comment_body:language'));
- $this->removeFieldMapping('path');
+ $this->addUnmigratedDestinations(array(
+ 'comment_body:format', 'comment_body:language',
+ 'language',
+ 'thread',
+ ));
+
$this->removeFieldMapping('pathauto');
}
}
// TIP: An easy way to simply migrate into a Drupal table (i.e., one defined
// through the Schema API) is to use the MigrateDestinationTable destination.
-// Just pass the table name to getKeySchema and the MigrateDestinationTable constructor.
+// Just pass the table name to getKeySchema and the MigrateDestinationTable
+// constructor.
class WineTableMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = 'Miscellaneous table data';
- $this->softDependencies = array('WineComment');
+
$table_name = 'migrate_example_wine_table_dest';
- $this->map = new MigrateSQLMap($this->machineName,
- array('fooid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- )
- ),
- MigrateDestinationTable::getKeySchema($table_name)
- );
+
$query = db_select('migrate_example_wine_table_source', 't')
->fields('t', array('fooid', 'field1', 'field2'));
$this->source = new MigrateSourceSQL($query);
$this->destination = new MigrateDestinationTable($table_name);
+ $this->map = new MigrateSQLMap($this->machineName,
+ array('fooid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationTable::getKeySchema($table_name)
+ );
// Mapped fields
$this->addFieldMapping('drupal_text', 'field1');
@@ -986,24 +1463,21 @@ class WineTableMigration extends AdvancedExampleMigration {
}
/**
- * This migration works with WinePrepMigration to make ensure auto_nodetitle
- * is re-enabled if we disabled it.
+ * This migration works with WinePrepMigration to make sure auto_nodetitle is
+ * re-enabled if we disabled it.
*/
class WineFinishMigration extends MigrationBase {
- public function __construct() {
- parent::__construct(MigrateGroup::getInstance('wine', array('default')));
- $this->description = t('If auto_nodetitle is present and was previously enabled,
- re-enable it');
- $this->dependencies = array('WineComment');
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description =
+ t('If auto_nodetitle is present and was previously enabled, re-enable it');
}
+
public function isComplete() {
- if (module_exists('auto_nodetitle')) {
- return TRUE;
- }
- else {
- return FALSE;
- }
+ // There is no incomplete state for this operation.
+ return TRUE;
}
+
public function import() {
if (!module_exists('auto_nodetitle')) {
if (WinePrepMigration::$wasEnabled) {
@@ -1011,11 +1485,13 @@ class WineFinishMigration extends MigrationBase {
self::displayMessage(t('Re-enabled auto_nodetitle module'), 'success');
}
else {
- self::displayMessage(t('auto_nodetitle was not originally enabled'), 'success');
+ self::displayMessage(t('auto_nodetitle was not originally enabled'),
+ 'success');
}
}
else {
- self::displayMessage(t('Auto_nodetitle module already enabled'), 'success');
+ self::displayMessage(t('Auto_nodetitle module already enabled'),
+ 'success');
}
return Migration::RESULT_COMPLETED;
}
@@ -1026,12 +1502,14 @@ class WineFinishMigration extends MigrationBase {
* to update existing content (in this case, revised wine ratings)
*/
class WineUpdatesMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Update wine ratings');
- $this->dependencies = array('WineWine');
- $this->softDependencies = array('WineFinish');
+ $query = db_select('migrate_example_wine_updates', 'w')
+ ->fields('w', array('wineid', 'rating'));
+ $this->source = new MigrateSourceSQL($query);
+ $this->destination = new MigrateDestinationNode('migrate_example_wine');
$this->map = new MigrateSQLMap($this->machineName,
array(
'wineid' => array(
@@ -1045,15 +1523,10 @@ class WineUpdatesMigration extends AdvancedExampleMigration {
MigrateDestinationNode::getKeySchema()
);
- $query = db_select('migrate_example_wine_updates', 'w')
- ->fields('w', array('wineid', 'rating'));
-
- $this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationNode('migrate_example_wine');
-
- // Indicate we're updating existing data. The default, Migration::SOURCE, would
- // cause existing nodes to be completely replaced by the source data. In this
- // case, the existing node will be loaded and only the rating altered.
+ // Indicate we're updating existing data. The default, Migration::SOURCE,
+ // would cause existing nodes to be completely replaced by the source data.
+ // In this case, the existing node will be loaded and only the rating
+ // altered.
$this->systemOfRecord = Migration::DESTINATION;
// Mapped fields
@@ -1066,7 +1539,8 @@ class WineUpdatesMigration extends AdvancedExampleMigration {
// No unmapped source fields
- // Unmapped destination fields
+ // Unmapped destination fields - these will be left unchanged in this case
+ // (normally they would be overwritten with their default values).
$this->addFieldMapping('uid');
$this->addFieldMapping('migrate_example_wine_varieties');
$this->addFieldMapping('migrate_example_wine_regions');
@@ -1076,42 +1550,67 @@ class WineUpdatesMigration extends AdvancedExampleMigration {
$this->addFieldMapping('sticky');
$this->addFieldMapping('created');
$this->addFieldMapping('changed');
- $this->addUnmigratedDestinations(array('title', 'revision_uid', 'status', 'promote',
- 'revision', 'log', 'language', 'tnid', 'is_new', 'body:format', 'body:summary',
- 'body:language', 'migrate_example_wine_regions:source_type',
- 'migrate_example_wine_regions:create_term', 'comment',
- 'migrate_example_wine_varieties:source_type', 'migrate_example_wine_varieties:create_term',
- 'migrate_example_wine_best_with:source_type', 'migrate_example_wine_best_with:create_term',
- 'field_migrate_example_image:language', 'field_migrate_example_image:destination_dir',
- 'field_migrate_example_image:alt', 'field_migrate_example_image:title',
- 'field_migrate_example_image:file_class', 'field_migrate_example_image:file_replace',
- 'field_migrate_example_image:preserve_files', 'field_migrate_example_image:destination_file',
- 'field_migrate_example_image:source_dir', 'field_migrate_example_top_vintag'));
+ $this->addUnmigratedDestinations(array(
+ 'body:format', 'body:summary', 'body:language',
+ 'comment',
+ 'field_migrate_example_image:alt',
+ 'field_migrate_example_image:destination_dir',
+ 'field_migrate_example_image:destination_file',
+ 'field_migrate_example_image:file_class',
+ 'field_migrate_example_image:file_replace',
+ 'field_migrate_example_image:language',
+ 'field_migrate_example_image:preserve_files',
+ 'field_migrate_example_image:source_dir',
+ 'field_migrate_example_image:title',
+ 'field_migrate_example_image:urlencode',
+ 'field_migrate_example_top_vintag',
+ 'is_new',
+ 'language',
+ 'log',
+ 'migrate_example_wine_best_with:source_type',
+ 'migrate_example_wine_best_with:create_term',
+ 'migrate_example_wine_best_with:ignore_case',
+ 'migrate_example_wine_regions:source_type',
+ 'migrate_example_wine_regions:create_term',
+ 'migrate_example_wine_regions:ignore_case',
+ 'migrate_example_wine_varieties:source_type',
+ 'migrate_example_wine_varieties:create_term',
+ 'migrate_example_wine_varieties:ignore_case',
+ 'promote',
+ 'revision',
+ 'revision_uid',
+ 'status',
+ 'title',
+ 'tnid',
+ 'translate',
+ ));
if (module_exists('statistics')) {
- $this->addUnmigratedDestinations(array('totalcount', 'daycount', 'timestamp'));
+ $this->addUnmigratedDestinations(
+ array('totalcount', 'daycount', 'timestamp'));
}
}
}
class WineCommentUpdatesMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = 'Update wine comments';
- $this->dependencies = array('WineComment');
- $this->softDependencies = array('WineUpdates');
- $this->map = new MigrateSQLMap($this->machineName,
- array('commentid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- )
- ),
- MigrateDestinationComment::getKeySchema()
- );
+
$query = db_select('migrate_example_wine_comment_updates', 'wc')
->fields('wc', array('commentid', 'subject'));
$this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationComment('comment_node_migrate_example_wine');
+ $this->destination =
+ new MigrateDestinationComment('comment_node_migrate_example_wine');
+ $this->map = new MigrateSQLMap($this->machineName,
+ array('commentid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ )
+ ),
+ MigrateDestinationComment::getKeySchema()
+ );
+
$this->systemOfRecord = Migration::DESTINATION;
// Mapped fields
@@ -1122,30 +1621,37 @@ class WineCommentUpdatesMigration extends AdvancedExampleMigration {
// No unmapped source fields
// Unmapped destination fields
- $this->addFieldMapping('name');
- $this->addFieldMapping('mail');
- $this->addFieldMapping('status');
- $this->addFieldMapping('nid');
- $this->addFieldMapping('uid');
- $this->addFieldMapping('pid');
- $this->addFieldMapping('comment_body');
- $this->addFieldMapping('hostname');
- $this->addFieldMapping('created');
- $this->addFieldMapping('changed');
- $this->addFieldMapping('homepage');
- $this->addUnmigratedDestinations(array('thread', 'language',
- 'comment_body:format', 'comment_body:language'));
- $this->removeFieldMapping('path');
+ $this->addUnmigratedDestinations(array(
+ 'changed',
+ 'comment_body', 'comment_body:format', 'comment_body:language',
+ 'created',
+ 'homepage',
+ 'hostname',
+ 'language',
+ 'mail',
+ 'name',
+ 'nid',
+ 'pid',
+ 'status',
+ 'thread',
+ 'uid',
+ ));
+
$this->removeFieldMapping('pathauto');
}
}
class WineVarietyUpdatesMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
- $this->description = t('Migrate varieties from the source database to taxonomy terms');
- $this->dependencies = array('WineVariety');
- $this->softDependencies = array('WineUpdates');
+ public function __construct($arguments) {
+ parent::__construct($arguments);
+ $this->description =
+ t('Migrate varieties from the source database to taxonomy terms');
+
+ $query = db_select('migrate_example_wine_variety_updates', 'wc')
+ ->fields('wc', array('categoryid', 'details'));
+ $this->source = new MigrateSourceSQL($query);
+ $this->destination =
+ new MigrateDestinationTerm('migrate_example_wine_varieties');
$this->map = new MigrateSQLMap($this->machineName,
array(
'categoryid' => array('type' => 'int',
@@ -1156,10 +1662,6 @@ class WineVarietyUpdatesMigration extends AdvancedExampleMigration {
MigrateDestinationTerm::getKeySchema()
);
- $query = db_select('migrate_example_wine_variety_updates', 'wc')
- ->fields('wc', array('categoryid', 'details'));
- $this->source = new MigrateSourceSQL($query);
- $this->destination = new MigrateDestinationTerm('migrate_example_wine_varieties');
$this->systemOfRecord = Migration::DESTINATION;
// Mapped fields
@@ -1170,36 +1672,36 @@ class WineVarietyUpdatesMigration extends AdvancedExampleMigration {
// Unmapped source fields
// Unmapped destination fields
- $this->addFieldMapping('name');
- $this->addFieldMapping('parent');
- $this->addFieldMapping('weight');
- $this->addFieldMapping('format');
- $this->addFieldMapping('parent_name')
- ->issueGroup(t('DNM'));
+ $this->addUnmigratedDestinations(array(
+ 'format',
+ 'name',
+ 'parent',
+ 'parent_name',
+ 'weight',
+ ));
}
}
-
class WineUserUpdatesMigration extends AdvancedExampleMigration {
- public function __construct() {
- parent::__construct();
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Account updates');
- $this->dependencies = array('WineUser');
- $this->softDependencies = array('WineUpdates');
- $this->map = new MigrateSQLMap($this->machineName,
- array('accountid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'Account ID.'
- )
- ),
- MigrateDestinationUser::getKeySchema()
- );
+
$query = db_select('migrate_example_wine_account_updates', 'wa')
->fields('wa', array('accountid', 'sex'));
$this->source = new MigrateSourceSQL($query);
$this->destination = new MigrateDestinationUser();
+ $this->map = new MigrateSQLMap($this->machineName,
+ array('accountid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Account ID.'
+ )
+ ),
+ MigrateDestinationUser::getKeySchema()
+ );
+
$this->systemOfRecord = Migration::DESTINATION;
// Mapped fields
@@ -1211,20 +1713,26 @@ class WineUserUpdatesMigration extends AdvancedExampleMigration {
// Unmapped source fields
// Unmapped destination fields
- $this->addFieldMapping('name');
- $this->addFieldMapping('status');
- $this->addFieldMapping('created');
- $this->addFieldMapping('access');
- $this->addFieldMapping('login');
- $this->addFieldMapping('mail');
- $this->addFieldMapping('pass');
- $this->addFieldMapping('roles');
- $this->addFieldMapping('signature');
- $this->addFieldMapping('signature_format');
- $this->addFieldMapping('init');
- $this->addUnmigratedDestinations(array('theme', 'timezone', 'language',
- 'picture', 'is_new', 'field_migrate_example_favbeers'));
- $this->removeFieldMapping('path');
+ $this->addUnmigratedDestinations(array(
+ 'access',
+ 'created',
+ 'data',
+ 'init',
+ 'is_new',
+ 'language',
+ 'login',
+ 'mail',
+ 'name',
+ 'pass',
+ 'picture',
+ 'role_names',
+ 'roles',
+ 'signature',
+ 'signature_format',
+ 'status',
+ 'theme',
+ 'timezone',
+ ));
}
public function prepare(stdClass $account, stdClass $row) {
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.install.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.install.inc
index ac4a760f..f360dc9a 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.install.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/wine.install.inc
@@ -65,26 +65,7 @@ function migrate_example_wine_uninstall() {
}
function migrate_example_wine_disable() {
- MigrationBase::deregisterMigration('WinePrep');
- Migration::deregisterMigration('WineFileCopy');
- Migration::deregisterMigration('WineFileBlob');
- Migration::deregisterMigration('WineRegion');
- Migration::deregisterMigration('WineUser');
- Migration::deregisterMigration('WineVariety');
- Migration::deregisterMigration('WineBestWith');
- Migration::deregisterMigration('WineProducer');
- Migration::deregisterMigration('WineProducerXML');
- Migration::deregisterMigration('WineProducerXMLPull');
- Migration::deregisterMigration('WineProducerMultiXML');
- Migration::deregisterMigration('WineWine');
- Migration::deregisterMigration('WineComment');
- MigrationBase::deregisterMigration('WineFinish');
- Migration::deregisterMigration('WineUpdates');
- Migration::deregisterMigration('WineUserUpdates');
- Migration::deregisterMigration('WineVarietyUpdates');
- Migration::deregisterMigration('WineCommentUpdates');
- Migration::deregisterMigration('WineRole');
- Migration::deregisterMigration('WineTable');
+ MigrateGroup::deregister('wine');
}
function migrate_example_wine_schema_wine() {
@@ -1212,7 +1193,7 @@ function migrate_example_wine_data_files() {
$query = db_insert('migrate_example_wine_files')
->fields($fields);
$data = array(
- array(1, 'http://drupal.org/sites/all/modules/drupalorg/drupalorg/images/association-individual.png', NULL, NULL, NULL),
+ array(1, 'http://placekitten.com/200/200', NULL, NULL, NULL),
array(2, 'http://cyrve.com/files/penguin.jpeg', 'Penguin alt', 'Penguin title', 1),
array(3, 'http://cyrve.com/files/rioja.jpeg', 'Rioja alt', 'Rioja title', 2),
array(4, 'http://cyrve.com/files/boutisse_0.jpeg', 'Boutisse alt', 'Boutisse title', 2),
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/0002.xml b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/0002.xml
new file mode 100644
index 00000000..380113b9
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/0002.xml
@@ -0,0 +1,7 @@
+
+
+ Château Latour
+ Makers of grand vin Chateau Latour, Les Forts de Latour and Pauillac
+ 3
+ Bordeaux
+
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/index2.xml b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/index2.xml
new file mode 100644
index 00000000..9dfa5002
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/index2.xml
@@ -0,0 +1,4 @@
+
+
+ 0002
+
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/producers3.xml b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/producers3.xml
new file mode 100644
index 00000000..62dabdd8
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/producers3.xml
@@ -0,0 +1,17 @@
+
+
+
+ 0009
+ Château Feytit Clinet
+ Good things, they say, come in small packages; this is certainly the case at Château Feyit Clinet, the maker of thelabel Pomerol.
+ 1
+ Pomerol
+
+
+ 0010
+ Château Doisy-Védrines
+ Blessed with ancient vines, the fruit here is often subject to high levels of Botrytis (or noble rot), which shrinks the grapes and concentrates sugar levels in the remaining juice.
+ 3
+ Barsac
+
+
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/producers4.xml b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/producers4.xml
new file mode 100644
index 00000000..aaeeca3e
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example/xml/producers4.xml
@@ -0,0 +1,17 @@
+
+
+
+ 0009
+ Château Bourgneuf
+ Showing attractive red and dark fruits, cherry and plum, it is rich and even slightly racy, before concluding with classic Pomerol concentration and focus.
+ 3
+ Pomerol
+
+
+ 0010
+ Château Doisy-Daëne
+ Medium bodied, with elegance rather than density there is a wide spectrum of flavours; apples, peaches, lemongrass and a touch of white spice. Delicious now.
+ 9
+ Barsac
+
+
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.features.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.features.inc
index 29ce604e..678121c3 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.features.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.features.inc
@@ -6,7 +6,7 @@
function migrate_example_baseball_node_info() {
$items = array(
'migrate_example_baseball' => array(
- 'name' => t('migrate_example_baseball'),
+ 'name' => t('Migrate example - Baseball'),
'base' => 'node_content',
'description' => t('A baseball box score'),
'has_title' => '1',
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.info b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.info
index 78bebfa4..8c26078b 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.info
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.info
@@ -21,12 +21,12 @@ features[field][] = "node-migrate_example_baseball-field_visiting_team"
features[node][] = "migrate_example_baseball"
files[] = "migrate_example_baseball.migrate.inc"
name = "migrate_example_baseball"
-package = "Migrate Examples"
+package = "Migration"
php = "5.2.4"
-; Information added by drupal.org packaging script on 2012-11-07
-version = "7.x-2.5"
+; Information added by Drupal.org packaging script on 2015-02-09
+version = "7.x-2.7"
core = "7.x"
project = "migrate"
-datestamp = "1352299007"
+datestamp = "1423521491"
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.install b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.install
index 68c23834..29cf296e 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.install
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.install
@@ -9,10 +9,10 @@ function migrate_example_baseball_enable() {
$path = dirname(__FILE__) . '/data';
migrate_example_baseball_get_files($path);
for ($i=0; $i<=9; $i++) {
- $file = 'gl200' . $i . '.txt';
+ $file = 'GL200' . $i . '.TXT';
Migration::registerMigration('GameBaseball',
pathinfo($file, PATHINFO_FILENAME),
- array('source_file' => $path . '/' . $file));
+ array('source_file' => $path . '/' . $file, 'group_name' => 'baseball'));
}
}
@@ -53,14 +53,7 @@ function migrate_example_baseball_uninstall() {
}
function migrate_example_baseball_disable() {
- for ($i=0; $i<=9; $i++) {
- $file = 'gl200' . $i . '.txt';
- Migration::deregisterMigration(pathinfo($file, PATHINFO_FILENAME));
- $filename = dirname(__FILE__) . '/data/' . $file;
- if (file_exists($filename)) {
- unlink($filename);
- }
- }
+ MigrateGroup::deregister('baseball');
}
/**
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.migrate.inc b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.migrate.inc
index ce7bb775..2e146e11 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.migrate.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_example_baseball/migrate_example_baseball.migrate.inc
@@ -13,17 +13,21 @@
function migrate_example_baseball_migrate_api() {
$api = array(
'api' => 2,
+ 'groups' => array(
+ 'baseball' => array(
+ 'title' => t('Baseball'),
+ ),
+ ),
);
return $api;
}
/**
- * A dynamic migration that is reused for each source CSV file.
+ * A migration that is reused for each source CSV file.
*/
-class GameBaseball extends DynamicMigration {
- public function __construct(array $arguments) {
- $this->arguments = $arguments;
- parent::__construct();
+class GameBaseball extends Migration {
+ public function __construct($arguments) {
+ parent::__construct($arguments);
$this->description = t('Import box scores from CSV file.');
// Create a map object for tracking the relationships between source rows
@@ -81,7 +85,7 @@ class GameBaseball extends DynamicMigration {
}
}
- function csvcolumns() {
+ protected function csvcolumns() {
// Note: Remember to subtract 1 from column number at http://www.retrosheet.org/gamelogs/glfields.txt
$columns[0] = array('start_date', 'Date of game');
$columns[3] = array('visiting_team', 'Visiting team');
@@ -102,7 +106,7 @@ class GameBaseball extends DynamicMigration {
return $columns;
}
- function prepareRow($row) {
+ public function prepareRow($row) {
// Collect all the batters into one multi-value field.
for ($i=1; $i <= 9; $i++ ) {
$key = "visiting_batter_$i";
@@ -123,11 +127,11 @@ class GameBaseball extends DynamicMigration {
PATHINFO_FILENAME));
}
- function fields() {
+ public function fields() {
return array(
'title' => 'A composite field made during prepareRow().',
'home_batters' => 'An array of batters, populated during prepareRow().',
'visiting_batters' => 'An array of batters, populated during prepareRow().',
);
}
-}
\ No newline at end of file
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.info b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.info
index 9dcce43d..97f6201c 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.info
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.info
@@ -1,14 +1,14 @@
name = "Migrate UI"
description = "UI for managing migration processes"
-package = "Development"
-;configure = admin/config/development/migrate
+package = "Migration"
+configure = admin/content/migrate/configure
core = 7.x
dependencies[] = migrate
-files[] = migrate_ui.module
+files[] = migrate_ui.wizard.inc
-; Information added by drupal.org packaging script on 2012-11-07
-version = "7.x-2.5"
+; Information added by Drupal.org packaging script on 2015-02-09
+version = "7.x-2.7"
core = "7.x"
project = "migrate"
-datestamp = "1352299007"
+datestamp = "1423521491"
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.install b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.install
new file mode 100644
index 00000000..da3de58f
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.install
@@ -0,0 +1,71 @@
+fields('s', array('weight'))
+ ->condition('name', 'node')
+ ->execute()
+ ->fetchField();
+ db_update('system')
+ ->fields(array('weight' => $node_weight + 1))
+ ->condition('name', 'migrate_ui')
+ ->execute();
+}
+
+/**
+ * If WordPress Migrate has background imports via drush enabled, copy the
+ * configuration to the new general Migrate support.
+ */
+function migrate_ui_update_7202() {
+ $drush_command = variable_get('wordpress_migrate_drush', '');
+ if ($drush_command) {
+ variable_set('migrate_drush_path', $drush_command);
+ // Consolidate these two variables into import method - 0 means immediate
+ // only, 1 means drush only, 2 means offer both options.
+ $import_method = variable_get('wordpress_migrate_import_method', 0);
+ $force_drush = variable_get('wordpress_migrate_force_drush', FALSE);
+ if (!$force_drush) {
+ $import_method = 2;
+ }
+ variable_set('migrate_import_method', $import_method);
+ variable_set('migrate_drush_mail',
+ variable_get('wordpress_migrate_notification', 0));
+ variable_set('migrate_drush_mail_subject',
+ variable_get('wordpress_migrate_notification_subject', ''));
+ variable_set('migrate_drush_mail_body',
+ variable_get('wordpress_migrate_notification_body', ''));
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.module b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.module
index b46667f3..57e91052 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.module
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.module
@@ -1,25 +1,12 @@
array(
- 'title' => t('Basic access to migration information'),
- ),
- );
-}
-
/**
* Implementation of hook_menu().
*/
@@ -27,67 +14,171 @@ function migrate_ui_menu() {
$items = array();
$items['admin/content/migrate'] = array(
- 'title' => 'Migrate',
'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
- 'description' => 'Monitor the creation of Drupal content from source data',
- 'page callback' => 'migrate_ui_dashboard',
+ 'title' => 'Migrate',
+ 'description' => 'Manage importing of data into your Drupal site',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('migrate_ui_migrate_dashboard'),
'access arguments' => array(MIGRATE_ACCESS_BASIC),
+ 'access callback' => 'user_access',
'file' => 'migrate_ui.pages.inc',
);
- $items['admin/content/migrate/dashboard'] = array(
- 'title' => 'Migrate',
+ $items['admin/content/migrate/groups'] = array(
+ 'title' => 'Dashboard',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
- $items['admin/content/migrate/registration'] = array(
- 'title' => 'Registration',
+
+ $items['admin/content/migrate/configure'] = array(
+ 'title' => 'Configuration',
'type' => MENU_LOCAL_TASK,
- 'description' => 'Configure class registration',
- 'page callback' => 'migrate_ui_registration',
- 'access arguments' => array(MIGRATE_ACCESS_BASIC),
- 'file' => 'migrate_ui.pages.inc',
- 'weight' => 5,
- );
- $items['admin/content/migrate/handlers'] = array(
- 'title' => 'Handlers',
- 'type' => MENU_LOCAL_TASK,
- 'description' => 'Configure migration handlers',
- 'page callback' => 'migrate_ui_handlers',
- 'access arguments' => array(MIGRATE_ACCESS_BASIC),
- 'file' => 'migrate_ui.pages.inc',
- 'weight' => 10,
- );
- $items['admin/content/migrate/messages/%migration'] = array(
- 'title callback' => '_migrate_ui_title',
- 'title arguments' => array(4),
- 'description' => 'View messages from a migration',
- 'page callback' => 'migrate_ui_messages',
- 'page arguments' => array(4),
- 'access arguments' => array(MIGRATE_ACCESS_BASIC),
- 'file' => 'migrate_ui.pages.inc',
- );
- $items['admin/content/migrate/%migration'] = array(
- 'title callback' => '_migrate_ui_title',
- 'title arguments' => array(3),
- 'page callback' => 'drupal_get_form',
- 'page arguments' => array('migrate_migration_info', 3),
- 'access arguments' => array(MIGRATE_ACCESS_BASIC),
+ 'description' => 'Configure migration settings',
+ 'page callback' => 'migrate_ui_configure',
+ 'access arguments' => array(MIGRATE_ACCESS_ADVANCED),
'file' => 'migrate_ui.pages.inc',
+ 'weight' => 100,
);
+ // Add tabs for each implemented migration wizard.
+ $wizards = migrate_ui_wizards();
+ foreach ($wizards as $wizard_class => $wizard) {
+ $items["admin/content/migrate/new/$wizard_class"] = array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => 'Import from @source_title',
+ 'title arguments' => array('@source_title' => $wizard->getSourceName()),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('migrate_ui_wizard', $wizard_class),
+ 'access arguments' => array(MIGRATE_ACCESS_BASIC),
+ 'file' => 'migrate_ui.wizard.inc',
+ );
+ }
+
+ $items['admin/content/migrate/groups/%'] =
+ array(
+ 'title callback' => 'migrate_ui_migrate_group_title',
+ 'title arguments' => array(4),
+ 'type' => MENU_NORMAL_ITEM,
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('migrate_ui_migrate_group', 4),
+ 'access arguments' => array(MIGRATE_ACCESS_BASIC),
+ 'file' => 'migrate_ui.pages.inc',
+ );
+
+ $items['admin/content/migrate/groups/%/%'] =
+ array(
+ 'title callback' => 'migrate_ui_migrate_migration_title',
+ 'title arguments' => array(5),
+ 'type' => MENU_NORMAL_ITEM,
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('migrate_migration_info', 4, 5),
+ 'access arguments' => array(MIGRATE_ACCESS_BASIC),
+ 'file' => 'migrate_ui.pages.inc',
+ );
+
+ $items['admin/content/migrate/groups/%/%/view'] =
+ array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ $items['admin/content/migrate/groups/%/%/edit'] =
+ array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => 'Edit',
+ 'description' => 'Edit migration mappings',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('migrate_ui_edit_mappings', 4, 5),
+ 'access arguments' => array(MIGRATE_ACCESS_ADVANCED),
+ 'file' => 'migrate_ui.pages.inc',
+ );
+
+ $items['admin/content/migrate/groups/%/%/messages'] =
+ array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => 'Messages',
+ 'description' => 'View messages from a migration',
+ 'page callback' => 'migrate_ui_messages',
+ 'page arguments' => array(4, 5),
+ 'access arguments' => array(MIGRATE_ACCESS_ADVANCED),
+ 'file' => 'migrate_ui.pages.inc',
+ );
+
return $items;
}
-// A menu load callback.
-function migration_load($machine_name) {
- if ($machine_name) {
- return Migration::getInstance($machine_name);
- }
+/**
+ * Title callback for the migrate group view page.
+ */
+function migrate_ui_migrate_group_title($group_name) {
+ $group = MigrateGroup::getInstance($group_name);
+ return $group->getTitle();
}
-function _migrate_ui_title($migration) {
- if (is_string($migration)) {
- $migration = migration_load($migration);
- }
- return $migration->getMachineName();
+/**
+ * Title callback for the migration view page.
+ */
+function migrate_ui_migrate_migration_title($migration_name) {
+ return $migration_name;
+}
+
+/**
+* Implements hook_theme()
+*
+* @return array
+*/
+function migrate_ui_theme() {
+ return array(
+ 'migrate_ui_field_mapping_form' => array(
+ 'arguments' => array('field_mappings' => NULL),
+ 'render element' => 'field_mappings',
+ 'file' => '/migrate_ui.pages.inc',
+ ),
+ 'migrate_ui_field_mapping_dependencies' => array(
+ 'arguments' => array('dependencies' => NULL),
+ 'render element' => 'dependencies',
+ 'file' => '/migrate_ui.pages.inc',
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_mail().
+ *
+ * @param $key
+ * @param $message
+ * @param $params
+ */
+function migrate_ui_mail($key, &$message, $params) {
+ $options['language'] = $message['language'];
+ user_mail_tokens($variables, array(), $options);
+ $langcode = $message['language']->language;
+ $subject = variable_get('migrate_drush_mail_subject', '');
+ $message['subject'] = t($subject, array(), array('langcode' => $langcode));
+ $body = variable_get('migrate_drush_mail_body', '');
+ $body .= "\n" . $params['output'];
+ $message['body'][] = t($body, array(), array('langcode' => $langcode));
+}
+
+/**
+ * Get info on all modules supporting a migration wizard.
+ *
+ * @return array
+ * key: machine_name for a particular wizard implementation. Used in the menu
+ * link.
+ * value: Wizard configuration array containing:
+ * source_title -
+ */
+function migrate_ui_wizards() {
+ $module_apis = migrate_get_module_apis();
+ $wizards = array();
+ foreach ($module_apis as $info) {
+ if (isset($info['wizard classes']) && is_array($info['wizard classes'])) {
+ foreach ($info['wizard classes'] as $wizard_class) {
+ $wizard_class = strtolower($wizard_class);
+ $wizards[$wizard_class] = new $wizard_class;
+ }
+ }
+ }
+ return $wizards;
}
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.pages.inc b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.pages.inc
index 5cd083c1..7760edf5 100644
--- a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.pages.inc
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.pages.inc
@@ -1,21 +1,15 @@
array('data' => t('Group')),
'status' => array('data' => t('Status')),
- 'machinename' => array('data' => t('Migration')),
- 'importrows' => array('data' => t('Total rows')),
- 'imported' => array('data' => t('Imported')),
- 'unimported' => array('data' => t('Unimported')),
- 'messages' => array('data' => t('Messages')),
- 'lastthroughput' => array('data' => t('Throughput')),
- 'lastimported' => array('data' => t('Last imported')),
+ 'source_system' => array('data' => t('Source system')),
);
+ $result = db_select('migrate_group', 'mg')
+ ->fields('mg', array('name', 'title', 'arguments'))
+ ->execute();
+ $rows = array();
+ foreach ($result as $group_row) {
+ $row = array();
+ $migration_result = db_select('migrate_status', 'ms')
+ ->fields('ms', array('machine_name', 'status', 'arguments'))
+ ->condition('group_name', $group_row->name)
+ ->execute();
+ if (!$migration_result) {
+ continue;
+ }
+ $status = t('Idle');
+ $idle = TRUE;
+ $machine_names = array();
+ foreach ($migration_result as $migration_row) {
+ switch ($migration_row->status) {
+ case MigrationBase::STATUS_IMPORTING:
+ $status = t('Importing');
+ $idle = FALSE;
+ break;
+ case MigrationBase::STATUS_ROLLING_BACK:
+ $status = t('Rolling back');
+ $idle = FALSE;
+ break;
+ case MigrationBase::STATUS_STOPPING:
+ $status = t('Stopping');
+ $idle = FALSE;
+ break;
+ }
+ $machine_names[] = $migration_row->machine_name;
+ }
+
+ // If we're not in the middle of an operaton, what's the status of the import?
+ if ($idle) {
+ $ready = TRUE;
+ $complete = TRUE;
+ foreach ($machine_names as $machine_name) {
+ $migration = Migration::getInstance($machine_name);
+ if (!$migration) {
+ continue;
+ }
+ if (method_exists($migration, 'sourceCount') && method_exists($migration, 'processedCount')) {
+ $source_count = $migration->sourceCount();
+ $processed_count = $migration->processedCount();
+ if ($processed_count > 0) {
+ $ready = FALSE;
+ if (!$complete) {
+ break;
+ }
+ }
+ if ($processed_count != $source_count) {
+ $complete = FALSE;
+ if (!$ready) {
+ break;
+ }
+ }
+ }
+ }
+ if ($ready) {
+ $status = t('Ready to import');
+ }
+ elseif ($complete) {
+ $status = t('Import complete');
+ }
+ else {
+ $status = t('Import incomplete, not currently running');
+ }
+ }
+
+ $row['status'] = $status;
+ $row['machinename'] =
+ l($group_row->title, 'admin/content/migrate/groups/' . $group_row->name);
+ $arguments = unserialize($group_row->arguments);
+ if (!empty($arguments['source_system'])) {
+ $row['source_system'] = $arguments['source_system'];
+ }
+ else {
+ $row['source_system'] = '';
+ }
+ $rows[$group_row->name] = $row;
+ }
+
+ $build['dashboard']['tasks'] = array(
+ '#type' => 'tableselect',
+ '#header' => $header,
+ '#options' => $rows,
+ '#tree' => TRUE,
+ '#empty' => t('No migration groups defined.'),
+ );
+ $build['operations'] = _migrate_ui_migrate_operations();
+
+ return $build;
+}
+
+/**
+ * Menu callback
+ */
+function migrate_ui_migrate_group($form, &$form_state, $group_name) {
+ $group = MigrateGroup::getInstance($group_name);
+ $build = array();
+
+ $header = array(
+ 'machinename' => array('data' => t('Task')),
+ 'status' => array('data' => t('Status')),
+ 'importrows' => array('data' => t('Items')),
+ 'imported' => array('data' => t('Imported')),
+ 'unprocessed' => array('data' => t('Unprocessed')),
+ );
+ if (user_access(MIGRATE_ACCESS_ADVANCED)) {
+ $header += array(
+ 'messages' => array('data' => t('Messages')),
+ 'lastthroughput' => array('data' => t('Throughput')),
+ 'lastimported' => array('data' => t('Last imported')),
+ );
+ }
+
$migrations = migrate_migrations();
$rows = array();
foreach ($migrations as $migration) {
+ $current_group = $migration->getGroup()->getName();
+ if ($current_group != $group_name) {
+ continue;
+ }
$row = array();
$has_counts = TRUE;
if (method_exists($migration, 'sourceCount')) {
@@ -61,10 +172,10 @@ function migrate_ui_dashboard_form($form, &$form_state) {
$imported = t('N/A');
}
if ($has_counts) {
- $unimported = $total - $processed;
+ $unprocessed = $total - $processed;
}
else {
- $unimported = t('N/A');
+ $unprocessed = t('N/A');
}
$status = $migration->getStatus();
switch ($status) {
@@ -90,116 +201,154 @@ function migrate_ui_dashboard_form($form, &$form_state) {
$row['status'] = $status;
$machine_name = $migration->getMachineName();
+ $name_length = strlen($machine_name);
+ $group_length = strlen($group_name);
+ if ($name_length != $group_length &&
+ !strncasecmp($group_name, $machine_name, $group_length)) {
+ $display_name = substr($machine_name, $group_length);
+ }
+ else {
+ $display_name = $machine_name;
+ }
$row['machinename'] =
- l($machine_name, 'admin/content/migrate/' . $machine_name);
+ l($display_name, "admin/content/migrate/groups/$group_name/$machine_name");
$row['importrows'] = $total;
$row['imported'] = $imported;
- $row['unimported'] = $unimported;
-
- if (is_subclass_of($migration, 'Migration')) {
- $num_messages = $migration->messageCount();
- $row['messages'] = $num_messages ? l($num_messages, 'admin/content/migrate/messages/' . $machine_name) : 0;
- }
- else {
- $row['messages'] = t('N/A');
- }
- if (method_exists($migration, 'getLastThroughput')) {
- $rate = $migration->getLastThroughput();
- if ($rate == '') {
- $row['lastthroughput'] = t('Unknown');
- }
- elseif ($status == MigrationBase::STATUS_IDLE) {
- $row['lastthroughput'] = t('!rate/min', array('!rate' => $rate));
+ $row['unprocessed'] = $unprocessed;
+ if (user_access(MIGRATE_ACCESS_ADVANCED)) {
+ if (is_subclass_of($migration, 'Migration')) {
+ $num_messages = $migration->messageCount();
+ $row['messages'] = $num_messages ?
+ l($num_messages, "admin/content/migrate/groups/$group_name/$machine_name/messages")
+ : 0;
}
else {
- if ($rate > 0) {
- $row['lastthroughput'] = t('!rate/min, !time remaining', array('!rate' => $rate, '!time' => format_interval((60*$unimported) / $rate)));
+ $row['messages'] = t('N/A');
+ }
+ if (method_exists($migration, 'getLastThroughput')) {
+ $rate = $migration->getLastThroughput();
+ if ($rate == '') {
+ $row['lastthroughput'] = t('Unknown');
}
else {
- $row['lastthroughput'] = t('!rate/min, unknown time remaining', array('!rate' => $rate));
+ $row['lastthroughput'] = t('!rate/min', array('!rate' => $rate));
}
}
+ else {
+ $row['lastthroughput'] = t('N/A');
+ }
+ $row['lastimported'] = $migration->getLastImported();
}
- else {
- $row['lastthroughput'] = t('N/A');
- }
- $row['lastimported'] = $migration->getLastImported();
$rows[$machine_name] = $row;
}
- $build['dashboard'] = array(
+ $build['dashboard']['migrations'] = array(
'#type' => 'tableselect',
'#header' => $header,
'#options' => $rows,
- '#empty' => t('No migrations'),
+ '#tree' => TRUE,
+ '#empty' => t('No tasks are defined for this migration group.'),
);
+ $build['operations'] = _migrate_ui_migrate_operations();
+
+ return $build;
+}
+
+/**
+ * @return array
+ */
+function _migrate_ui_migrate_operations() {
// Build the 'Update options' form.
- $build['operations'] = array(
+ $operations = array(
'#type' => 'fieldset',
'#title' => t('Operations'),
);
- $options = array(
- 'import' => t('Import'),
- 'rollback' => t('Rollback'),
- 'rollback_and_import' => t('Rollback and import'),
+ //
+ switch (variable_get('migrate_import_method', 0)) {
+ case 1:
+ $options = array(
+ 'import_background' => t('Import'),
+ 'rollback_background' => t('Rollback'),
+ );
+ break;
+ case 2:
+ $options = array(
+ 'import_immediate' => t('Import immediately'),
+ 'import_background' => t('Import in background'),
+ 'rollback_immediate' => t('Rollback immediately'),
+ 'rollback_background' => t('Rollback in background'),
+ );
+ break;
+ case 0:
+ default:
+ $options = array(
+ 'import_immediate' => t('Import'),
+ 'rollback_immediate' => t('Rollback'),
+ );
+ break;
+ }
+
+ $options += array(
'stop' => t('Stop'),
'reset' => t('Reset'),
+ 'deregister' => t('Remove migration settings'),
);
- $build['operations']['operation'] = array(
+ $operations['operation'] = array(
'#type' => 'select',
'#title' => t('Operation'),
'#title_display' => 'invisible',
'#options' => $options,
);
- $build['operations']['submit'] = array(
+ $operations['submit'] = array(
'#type' => 'submit',
'#value' => t('Execute'),
- '#validate' => array('migrate_ui_dashboard_validate'),
- '#submit' => array('migrate_ui_dashboard_submit'),
+ '#validate' => array('migrate_ui_migrate_validate'),
+ '#submit' => array('migrate_ui_migrate_submit'),
);
- $build['operations']['description'] = array(
+ $operations['description'] = array(
'#prefix' => '',
'#markup' => t(
- 'Choose an operation to run on all migrations selected above:
+ 'Choose an operation to run on all selections above:
- - Import - Imports all previously unimported records from the source, plus
+
- Import
Imports all previously unprocessed records from the source, plus
any records marked for update, into destination Drupal objects.
- - Rollback - Deletes all Drupal objects created by the migration.
- - Rollback and import - Performs the Rollback operation, immediately
- followed by the Import operation.
- - Stop - Cleanly interrupts any import or rollback processes that may
+
- Rollback
Deletes all Drupal objects created by the import.
+ - Stop
Cleanly interrupts any import or rollback processes that may
currently be running.
- - Reset - Sometimes a migration process may fail to stop cleanly, and be
+
- Reset
Sometimes a process may fail to stop cleanly, and be
left stuck in an Importing or Rolling Back status. Choose Reset to clear
the status and permit other operations to proceed.
+ - Remove migration settings
Removes all information about a migration group
+ or task, while preserving any content that has already been imported.
'
),
'#postfix' => '
',
);
- $build['operations']['options'] = array(
+ $operations['options'] = array(
'#type' => 'fieldset',
'#title' => t('Options'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
+ '#access' => user_access(MIGRATE_ACCESS_ADVANCED),
);
- $build['operations']['options']['update'] = array(
+ $operations['options']['update'] = array(
'#type' => 'checkbox',
'#title' => t('Update'),
- '#description' => t('Check this box to update all previously-migrated content
+ '#description' => t('Check this box to update all previously-imported content
in addition to importing new content. Leave unchecked to only import
new content'),
);
- $build['operations']['options']['force'] = array(
+ $operations['options']['force'] = array(
'#type' => 'checkbox',
'#title' => t('Ignore dependencies'),
'#description' => t('Check this box to ignore dependencies when running imports
- - all migrations will run whether or not their dependent migrations have
+ - all tasks will run whether or not their dependent tasks have
completed.'),
);
- $build['operations']['options']['limit'] = array(
+ $operations['options']['limit'] = array(
'#tree' => TRUE,
'#type' => 'fieldset',
'#attributes' => array('class' => array('container-inline')),
@@ -215,56 +364,110 @@ function migrate_ui_dashboard_form($form, &$form_state) {
'seconds' => t('seconds'),
),
'#description' => t('Set a limit of how many items to process for
- each migration, or how long each should run.'),
+ each migration task, or how long each should run.'),
),
);
- return $build;
+ return $operations;
}
/**
* Validate callback for the dashboard form.
*/
-function migrate_ui_dashboard_validate($form, &$form_state) {
- // Error if there are no items to select.
- if (!is_array($form_state['values']['dashboard']) || !count(array_filter($form_state['values']['dashboard']))) {
- form_set_error('', t('No items selected.'));
+function migrate_ui_migrate_validate($form, &$form_state) {
+ $migrations = _migrate_ui_selected_migrations($form_state['values']);
+ if (empty($migrations)) {
+ if (isset($form_state['values']['migrations'])) {
+ form_set_error('', t('No items selected.'));
+ }
}
}
+/**
+ * Determine what migrations are selected (whether directly on a group page, or
+ * via group selections on the dashboard).
+ *
+ * @param $values
+ *
+ * @return array
+ */
+function _migrate_ui_selected_migrations($values) {
+ if (isset($values['migrations'])) {
+ // From the specific group page, just take them in order (they were already
+ // sorted by dependencies).
+ $machine_names = array_filter($values['migrations']);
+ }
+ else {
+ // From the groups page, we need to use migrate_migrations to be sure we've
+ // got the tasks for each group in dependency order.
+ $tasks = array_filter($values['tasks']);
+ $migrations = migrate_migrations();
+ $machine_names = array();
+ foreach ($migrations as $migration) {
+ $group_name = $migration->getGroup()->getName();
+ if (in_array($group_name, $tasks)) {
+ $machine_names[] = $migration->getMachineName();
+ }
+ }
+ }
+ return $machine_names;
+}
+
/**
* Submit callback for the dashboard form.
*/
-function migrate_ui_dashboard_submit($form, &$form_state) {
- $operation = $form_state['values']['operation'];
- $limit = $form_state['values']['limit'];
- $update = $form_state['values']['update'];
- $force = $form_state['values']['force'];
- $machine_names = array_filter($form_state['values']['dashboard']);
+function migrate_ui_migrate_submit($form, &$form_state) {
+ $values = $form_state['values'];
+ $operation = $values['operation'];
+
+ $limit = $values['limit'];
+
+ if (isset($values['update'])) {
+ $update = $values['update'];
+ }
+ else {
+ $update = 0;
+ }
+ if (isset($values['force'])) {
+ $force = $values['force'];
+ }
+ else {
+ $force = 0;
+ }
+ $machine_names = _migrate_ui_selected_migrations($values);
$operations = array();
// Rollback in reverse order.
- if (in_array($operation, array('rollback', 'rollback_and_import'))) {
+ if (in_array($operation, array('rollback_immediate', 'deregister'))) {
$machine_names = array_reverse($machine_names);
- foreach ($machine_names as $machine_name) {
- $operations[] = array('migrate_ui_batch', array('rollback', $machine_name, $limit, $force));
- }
- // Reset order of machines names in preparation for final operation.
- $machine_names = array_reverse($machine_names);
- $operation = ($operation == 'rollback_and_import') ? 'import' : NULL;
}
- // Perform non-rollback operation, if one exists.
- if ($operation) {
- foreach ($machine_names as $machine_name) {
- $migration = Migration::getInstance($machine_name);
+ // Special case: when deregistering a group, go through the group API
+ if ($operation == 'deregister' && isset($values['tasks'])) {
+ foreach ($values['tasks'] as $task) {
+ MigrateGroup::deregister($task);
+ }
+ return;
+ }
+
+ $drush_arguments = array();
+ foreach ($machine_names as $machine_name) {
+ $migration = Migration::getInstance($machine_name);
+ if ($migration) {
switch ($operation) {
- case 'import':
+ case 'import_immediate':
// Update (if necessary) once, before starting
if ($update && method_exists($migration, 'prepareUpdate')) {
$migration->prepareUpdate();
}
- $operations[] = array('migrate_ui_batch', array($operation, $machine_name, $limit, $force));
+ $operations[] = array('migrate_ui_batch', array('import', $machine_name, $limit, $force));
+ break;
+ case 'rollback_immediate':
+ $operations[] = array('migrate_ui_batch', array('rollback', $machine_name, $limit, $force));
+ break;
+ case 'import_background':
+ case 'rollback_background':
+ $drush_arguments[] = $machine_name;
break;
case 'stop':
$migration->stopProcess();
@@ -272,26 +475,70 @@ function migrate_ui_dashboard_submit($form, &$form_state) {
case 'reset':
$migration->resetStatus();
break;
+ case 'deregister':
+ migrate_ui_deregister_migration($machine_name);
+ break;
}
}
}
- // Only rollback and import operations will need to go through Batch API.
+ // Only immediate rollback and import operations will need to go through Batch API.
if (count($operations) > 0) {
$batch = array(
'operations' => $operations,
- 'title' => t('Migration processing'),
+ 'title' => t('Import processing'),
'file' => drupal_get_path('module', 'migrate_ui') . '/migrate_ui.pages.inc',
- 'init_message' => t('Starting migration process'),
+ 'init_message' => t('Starting import process'),
'progress_message' => t(''),
- 'error_message' => t('An error occurred. Some or all of the migrate processing has failed.'),
+ 'error_message' => t('An error occurred. Some or all of the import processing has failed.'),
'finished' => 'migrate_ui_batch_finish',
);
batch_set($batch);
}
+ elseif (count($drush_arguments) > 0) {
+ $drush_path = trim(variable_get('migrate_drush_path', ''));
+ $uri = $GLOBALS['base_url'];
+ $uid = $GLOBALS['user']->uid;
+ if ($operation == 'import_background') {
+ $command = 'mi';
+ $log_suffix = '.import.log';
+ }
+ else {
+ $command = 'mr';
+ $log_suffix = '.rollback.log';
+ }
+ $migrations = implode(',', $drush_arguments);
+ $drush_command = "$drush_path $command $migrations --user=$uid --uri=$uri " .
+ '--root=' . DRUPAL_ROOT;
+ if ($force) {
+ $drush_command .= ' --force';
+ }
+ if ($update) {
+ $drush_command .= ' --update';
+ }
+ if (variable_get('migrate_drush_mail', 0)) {
+ $drush_command .= ' --notify';
+ }
+ if (!empty($limit['value'])) {
+ $limit = $limit['value'] . ' ' . $limit['unit'];
+ $drush_command .= " --limit=\"$limit\"";
+ }
+ $log_file = '/tmp/' . $drush_arguments[0] . $log_suffix;
+ $drush_command .= " >$log_file 2>&1 &";
+ exec($drush_command, $output, $status);
+ if (variable_get('migrate_drush_mail', 0)) {
+ drupal_set_message('Your operation is running in the background. You will receive an email message when it is complete.');
+ }
+ else {
+ drupal_set_message('Your operation is running in the background. You may '.
+ 'refresh the dashboard page to check on its progress.');
+ }
+ }
}
/**
+ * Implements callback_batch_operation().
+ *
* Process all enabled migration processes in a browser, using the Batch API
* to break it into manageable chunks.
*
@@ -314,12 +561,19 @@ function migrate_ui_batch($operation, $machine_name, $limit, $force = FALSE, &$c
}
$migration = Migration::getInstance($machine_name);
+ if (!$migration) {
+ $context['finished'] = 1;
+ return;
+ }
// Messages generated by migration processes will be captured in this global
global $_migrate_messages;
$_migrate_messages = array();
Migration::setDisplayFunction('migrate_ui_capture_message');
+ // Try to allocate enough time to run the full operation.
+ $migration->setBatchTimeLimit();
+
// Perform the requested operation
switch ($operation) {
case 'import':
@@ -351,11 +605,13 @@ function migrate_ui_batch($operation, $machine_name, $limit, $force = FALSE, &$c
}
break;
case MigrationBase::RESULT_SKIPPED:
- $_migrate_messages[] = t("Skipped !name due to unfulfilled dependencies: !depends",
- array(
- '!name' => $machine_name,
- '!depends' => implode(", ", $migration->incompleteDependencies()),
- ));
+ $_migrate_messages[] = array(
+ 'message' => t("Skipped !name due to unfulfilled dependencies: !depends",
+ array(
+ '!name' => $machine_name,
+ '!depends' => implode(", ", $migration->incompleteDependencies()),
+ ))
+ );
$context['finished'] = 1;
break;
case MigrationBase::RESULT_STOPPED:
@@ -369,20 +625,22 @@ function migrate_ui_batch($operation, $machine_name, $limit, $force = FALSE, &$c
}
// Add any messages generated in this batch to the cumulative list
- foreach ($_migrate_messages as $message) {
- $context['results'][] = $message;
+ foreach ($_migrate_messages as $message_data) {
+ $context['results'][] = $message_data;
}
// While in progress, show the cumulative list of messages
$full_message = '';
- foreach ($context['results'] as $message) {
- $full_message .= $message . '
';
+ foreach ($context['results'] as $message_data) {
+ $full_message .= $message_data['message'] . '
';
}
$context['message'] = $full_message;
}
/**
- * Batch API finished callback - report results
+ * Implements callback_batch_finished().
+ *
+ * Report results.
*
* @param $success
* Ignored
@@ -393,76 +651,47 @@ function migrate_ui_batch($operation, $machine_name, $limit, $force = FALSE, &$c
*/
function migrate_ui_batch_finish($success, $results, $operations) {
unset($results['stopped']);
+
foreach ($results as $result) {
- drupal_set_message($result);
+ // Migration::progressMessage() uses message levels that don't match what
+ // drupal_set_message() expects.
+ if (isset($result['level'])) {
+ if ($result['level'] == 'completed') {
+ $level = 'status';
+ }
+ elseif ($result['level'] == 'failed') {
+ $level = 'error';
+ }
+ else {
+ $level = $result['level'];
+ }
+ }
+ else {
+ $level = NULL;
+ }
+
+ drupal_set_message($result['message'], $level);
}
}
function migrate_ui_capture_message($message, $level) {
if ($level != 'debug') {
+ // Store each message as an array with keys 'message' and 'level'.
global $_migrate_messages;
- $_migrate_messages[] = $message;
- }
-}
-
-/**
- * Menu callback for messages page
- */
-function migrate_ui_messages($migration) {
- $build = $rows = array();
-
- $header = array(
- array('data' => t('Source ID'), 'field' => 'sourceid1', 'sort' => 'asc'),
- array('data' => t('Level'), 'field' => 'level'),
- array('data' => t('Message'), 'field' => 'message'),
- );
-
- if (is_string($migration)) {
- $migration = migration_load($migration);
- }
-
- // TODO: need a general MigrateMap API
- $messages = $migration->getMap()->getConnection()
- ->select($migration->getMap()->getMessageTable(), 'msg')
- ->extend('PagerDefault')
- ->extend('TableSort')
- ->orderByHeader($header)
- ->limit(500)
- ->fields('msg')
- ->execute();
-
- foreach ($messages as $message) {
- $classes[] = $message->level <= MigrationBase::MESSAGE_WARNING ? 'migrate-error' : '';
- $rows[] = array(
- array('data' => $message->sourceid1, 'class' => $classes), // TODO: deal with compound keys
- array('data' => $migration->getMessageLevelName($message->level), 'class' => $classes),
- array('data' => $message->message, 'class' => $classes),
+ $_migrate_messages[] = array(
+ 'message' => $message,
+ 'level' => $level,
);
- unset($classes);
}
-
- $build['messages'] = array(
- '#theme' => 'table',
- '#header' => $header,
- '#rows' => $rows,
- '#empty' => t('No messages'),
- '#attached' => array(
- 'css' => array(drupal_get_path('module', 'migrate_ui') . '/migrate_ui.css'),
- ),
- );
- $build['migrate_ui_pager'] = array('#theme' => 'pager');
- return $build;
}
/**
* Menu callback function for migration view page.
*/
-function migrate_migration_info($form, $form_state, $migration) {
- if (is_string($migration)) {
- $migration = migration_load($migration);
- }
+function migrate_migration_info($form, $form_state, $group_name, $migration_name) {
+ $migration = Migration::getInstance($migration_name);
- $has_mappings = method_exists($migration, 'getFieldMappings');
+ $has_mappings = $migration && method_exists($migration, 'getFieldMappings');
$form = array();
if ($has_mappings) {
@@ -495,6 +724,10 @@ function migrate_migration_info($form, $form_state, $migration) {
'#group' => 'detail',
);
+ if (!$migration) {
+ return $form;
+ }
+
$team = array();
foreach ($migration->getTeam() as $member) {
$email_address = $member->getEmailAddress();
@@ -527,6 +760,12 @@ function migrate_migration_info($form, $form_state, $migration) {
);
}
+ $form['overview']['group'] = array(
+ '#title' => t('Group:'),
+ '#markup' => $migration->getGroup()->getTitle(),
+ '#type' => 'item',
+ );
+
if ($has_mappings) {
switch ($migration->getSystemOfRecord()) {
case Migration::SOURCE:
@@ -559,8 +798,8 @@ function migrate_migration_info($form, $form_state, $migration) {
'#title' => t('Destination'),
'#group' => 'detail',
'#description' =>
- t('These are the fields available in the destination of this
- migration. The machine names listed here are those available to be used
+ t('
These are the fields available in the destination of this migration
+ task. The machine names listed here are those available to be used
as the first parameter to $this->addFieldMapping() in your Migration
class constructor. Unmapped fields are red.
'),
);
@@ -600,8 +839,8 @@ function migrate_migration_info($form, $form_state, $migration) {
'#title' => t('Source'),
'#group' => 'detail',
'#description' =>
- t('These are the fields available from the source of this
- migration. The machine names listed here are those available to be used
+ t('
These are the fields available from the source of this migration
+ task. The machine names listed here are those available to be used
as the second parameter to $this->addFieldMapping() in your Migration
class constructor. Unmapped fields are red.
'),
);
@@ -695,9 +934,16 @@ function migrate_migration_info($form, $form_state, $migration) {
$priority = t('OK');
$classes[] = 'migrate-priority-' . 1;
}
+ $destination_field = $mapping->getDestinationField();
+ $source_field = $mapping->getSourceField();
+ // Highlight any mappings overridden in the database.
+ if ($mapping->getMappingSource() == MigrateFieldMapping::MAPPING_SOURCE_DB) {
+ $destination_field = "$destination_field";
+ $source_field = "$source_field";
+ }
$row = array(
- array('data' => $mapping->getDestinationField(), 'class' => $classes),
- array('data' => $mapping->getSourceField(), 'class' => $classes),
+ array('data' => $destination_field, 'class' => $classes),
+ array('data' => $source_field, 'class' => $classes),
array('data' => $default, 'class' => $classes),
array('data' => $mapping->getDescription(), 'class' => $classes),
array('data' => $priority, 'class' => $classes),
@@ -716,101 +962,835 @@ function migrate_migration_info($form, $form_state, $migration) {
}
/**
- * Menu callback
+ * Page callback to edit field mappings for a given migration.
+ *
+ * @param $form
+ * @param $form_state
+ * @param $group_name
+ * @param $migration_name
+ *
+ * @return array
*/
-function migrate_ui_registration() {
- drupal_set_title(t('Migration class registration'));
- return drupal_get_form('migrate_ui_registration_form');
-}
+function migrate_ui_edit_mappings($form, $form_state, $group_name,
+ $migration_name) {
+ drupal_set_title(t('Edit !migration', array('!migration' => $migration_name)));
-/**
- * Form for reviewing migrations.
- */
-function migrate_ui_registration_form($form, &$form_state) {
- $build = array();
+ $form = array();
+ $form['#tree'] = TRUE;
+ $form['machine_name'] = array(
+ '#type' => 'value',
+ '#value' => $migration_name,
+ );
- if (variable_get('migrate_disable_autoregistration', FALSE)) {
- $description = t('You currently have automatic class registration turned off.
- This means that any Migration classes, destination handler classes, or
- field handlers must be explicitly registered, either through hook_migrate_api()
- or by calling MigrationBase::registerMigration(). You may enable automatic
- class registration by clicking this button - however, it\'s important to
- note that in some environments registration may fail with errors like
- "Class \'views_handler_field\' not found".');
- $button_label = t('Enable automatic registration');
+ $migration = Migration::getInstance($migration_name);
+ if (!$migration) {
+ return $form;
}
- else {
- $description = t('You currently have automatic class registration turned on.
- This means that any Migration classes, destination handler classes, or
- field handlers not explicitly registered, either through hook_migrate_api()
- or by calling MigrationBase::registerMigration(), can be registered by
- clicking the Register button below. It\'s important to
- note that in some environments registration may fail with errors like
- "Class \'views_handler_field\' not found" - in those cases, you
- can click the Disable automatic registration button below,
- but you must be sure that your classes are explicitly registered.');
- $button_label = t('Disable automatic registration');
+
+ $source_migration_options = array('-1' => t(''),);
+ $all_dependencies = array();
+ foreach (migrate_migrations() as $machine_name => $migration_info) {
+ $source_migration_options[$machine_name] = $machine_name;
+ $dependencies = $migration_info->getDependencies();
+ if (!empty($dependencies)) {
+ $all_dependencies[$machine_name] = $dependencies;
+ }
}
- $build['registration'] = array(
+
+ if (is_a($migration, 'Migration')) {
+ $source_fields = $migration->getSource()->fields();
+ $dest_fields = $migration->getDestination()->fields($migration);
+ $dest_key = $migration->getDestination()->getKeySchema();
+ $field_mappings = $migration->getFieldMappings();
+
+ $form['field_mappings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Field mappings'),
+ '#collapsible' => TRUE,
+ '#description' => t('For each field available in your Drupal destination, select the source field used to populate it. You can enter a default value to be applied to the destination when there is no source field, or the source field is empty in a given source item. Check the DNM (Do Not Migrate) box for destination fields you do not want populated by migration. Note that any changes you make here override any field mappings defined by the underlying migration module. Clicking the Revert button will remove all such overrides.'),
+ '#theme' => array('migrate_ui_field_mapping_form'),
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+
+ $form['source_fields'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Source fields'),
+ '#collapsible' => TRUE,
+ '#description' => t('Check off any source fields that are not being mapped to destination fields. They will be removed from the select lists above.'),
+ );
+
+ $base_options = array(
+ '-1' => t(''),
+ );
+
+ $field_options = array();
+ foreach ($source_fields as $name => $description) {
+ // Clean up the source field description
+ if (is_array($description)) {
+ $description = reset($description);
+ }
+ $description = strip_tags($description);
+ // Limit the description length
+ if (strlen($description) > 50) {
+ $short_description = substr($description, 0, 50) . '...';
+ }
+ else {
+ $short_description = $description;
+ }
+ if (user_access(MIGRATE_ACCESS_ADVANCED)) {
+ $label_format = '!description [!source_field]';
+ }
+ else {
+ $label_format = '!description';
+ }
+ $label = t($label_format,
+ array('!source_field' => $name, '!description' => $description));
+ $short_label = t($label_format,
+ array('!source_field' => $name, '!description' => $short_description));
+
+ $dnm_value = 0;
+
+ // Check for a click of the DNM box...
+ if (isset($form_state['values']) &&
+ $form_state['values']['source_fields'][$name] == 1) {
+ $dnm_value = 1;
+ }
+ else {
+ // ... or a source-only mapping with the DNM issue group.
+ foreach ($field_mappings as $mapping) {
+ if ($mapping->getSourceField() == $name &&
+ $mapping->getIssueGroup() == t('DNM')) {
+ $dnm_value = 1;
+ }
+ }
+ }
+
+ // So, if this field is not marked DNM, include it in possible source
+ // options in the field mappings.
+ if (!$dnm_value) {
+ $field_options[$name] = $short_label;
+ }
+
+ $form['source_fields'][$name] = array(
+ '#type' => 'checkbox',
+ '#title' => $label,
+ '#default_value' => $dnm_value,
+ );
+ }
+
+ if (isset($field_mappings['is_new'])) {
+ $default_is_new = $field_mappings['is_new']->getDefaultValue();
+ } else {
+ $default_is_new = 0;
+ }
+ foreach ($dest_fields as $name => $description) {
+ // Don't map the destination key unless is_new = 1 (ie, TRUE)
+ if (isset($dest_key[$name]) && $default_is_new == 0) {
+ continue;
+ }
+
+ if (is_array($description)) {
+ $description = reset($description);
+ }
+
+ $options = $base_options + $field_options;
+
+ $default_mapping = '-1';
+ $default_value = '';
+ if (isset($field_mappings[$name])) {
+ $mapping = $field_mappings[$name];
+ }
+ else {
+ $mapping = NULL;
+ }
+
+ // If the DNM box has been clicked, make sure we clear the mapping and
+ // default value fields.
+ if (isset($form_state['values']) &&
+ $form_state['values']['field_mappings'][$name]['issue_group'] == 1) {
+ $dnm_value = 1;
+ }
+ else {
+ // Determine the default field mapping - if we have an existing one, use
+ // that, otherwise try to match on name.
+ if (isset($field_mappings[$name])) {
+ if ($mapping->getSourceField()) {
+ $default_mapping = $mapping->getSourceField();
+ }
+ $default_value = $mapping->getDefaultValue();
+ $dnm_value = ($mapping->getIssueGroup() == t('DNM'));
+ }
+ else {
+ $dnm_value = 0;
+ }
+ }
+
+ // Only show the machine name to advanced users.
+ if (user_access(MIGRATE_ACCESS_ADVANCED)) {
+ $label = '!description [!field_name]';
+ }
+ else {
+ $label = '!description';
+ }
+
+ // Indent subfields and options to show their relationship to the parent.
+ if (strpos($name, ':') > 0) {
+ $description = ' ' . $description;
+ }
+
+ $form['field_mappings'][$name]['issue_group'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => $dnm_value,
+ );
+
+ $form['field_mappings'][$name]['mapping'] = array(
+ '#type' => 'select',
+ '#title' => t($label,
+ array('!description' => $description, '!field_name' => $name)),
+ '#options' => $options,
+ '#default_value' => $default_mapping,
+ );
+
+ $form['field_mappings'][$name]['default_value'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $default_value,
+ '#size' => 20,
+ );
+
+ $source_migration = NULL;
+ if ($mapping) {
+ $source_migration = $mapping->getSourceMigration();
+ }
+ if (!$source_migration) {
+ $source_migration = '-1';
+ }
+ $form['field_mappings'][$name]['source_migration'] = array(
+ '#type' => 'select',
+ '#options' => $source_migration_options,
+ '#default_value' => $source_migration,
+ );
+ }
+ }
+
+ unset($source_migration_options[-1]);
+ unset($source_migration_options[$migration_name]);
+
+ $form['dependencies'] = array(
'#type' => 'fieldset',
- '#title' => t('Migration registration'),
- '#description' => $description,
+ '#title' => t('Dependencies'),
+ '#collapsible' => TRUE,
+ '#collapsed' => is_a($migration, 'Migration'),
+ '#theme' => 'migrate_ui_field_mapping_dependencies',
+ '#description' => t('Set any dependencies on other migrations here. A hard dependency means that this migration is not allowed to run until the dependent migration has completed (without forcing it). A soft dependency places no such requirement - it simply affects the default order of migration. Note that any migrations that would lead to circular dependencies are not listed here.'),
);
- $build['registration']['auto_register'] = array(
- '#type' => 'submit',
- '#value' => $button_label,
- '#submit' => array('migrate_ui_configure_register_auto_register'),
+ // Get the list of possible dependencies - it's the same as the list of
+ // possible source migrations, minus the empty choice and ourselves.
+
+ $dependency_options = array(
+ 0 => t('None'),
+ 1 => t('Hard'),
+ 2 => t('Soft'),
);
- if (!variable_get('migrate_disable_autoregistration', FALSE)) {
- $build['registration']['submit'] = array(
- '#type' => 'submit',
- '#value' => t('Register'),
- '#submit' => array('migrate_ui_configure_register_submit'),
+ // Remove any migrations depending on us, directly or indirectly. First, get
+ // a list of such migrations.
+ $descendent_migrations = _migrate_ui_get_descendents($migration_name,
+ $all_dependencies);
+ $source_migration_options = array_diff_key($source_migration_options,
+ $descendent_migrations);
+ foreach ($source_migration_options as $machine_name) {
+ if (in_array($machine_name, $migration->getHardDependencies())) {
+ $default_value = 1;
+ }
+ elseif (in_array($machine_name, $migration->getSoftDependencies())) {
+ $default_value = 2;
+ }
+ else {
+ $default_value = 0;
+ }
+ $form['dependencies'][$machine_name] = array(
+ '#type' => 'select',
+ '#title' => $machine_name,
+ '#default_value' => $default_value,
+ '#options' => $dependency_options,
);
}
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Apply changes'),
+ );
+
+ $form['revert'] = array(
+ '#type' => 'submit',
+ '#value' => t('Revert'),
+ '#submit' => array('migrate_ui_edit_mappings_revert'),
+ );
+
+ return $form;
+}
+
+/**
+ * Generate a list of all migrations dependent on a given migration.
+ *
+ * @param $migration_name
+ * @param array $all_dependencies
+ *
+ * @return array
+ */
+function _migrate_ui_get_descendents($migration_name, array $all_dependencies) {
+ $descendents = array();
+ foreach ($all_dependencies as $machine_name => $dependencies) {
+ if (in_array($migration_name, $dependencies)) {
+ $descendents[$machine_name] = $machine_name;
+ $descendents += _migrate_ui_get_descendents($machine_name,
+ $all_dependencies);
+ }
+ }
+ return $descendents;
+}
+
+/**
+ * Submit callback for the edit mappings form. Save any choices made in the
+ * UI which override mappings made in code.
+ *
+ * @param $form
+ * @param $form_state
+ */
+function migrate_ui_edit_mappings_submit(&$form, &$form_state) {
+ $machine_name = $form_state['values']['machine_name'];
+ $row = db_select('migrate_status', 'ms')
+ ->fields('ms', array('arguments', 'class_name', 'group_name'))
+ ->condition('machine_name', $machine_name)
+ ->execute()
+ ->fetchObject();
+ $class_name = $row->class_name;
+ $group_name = $row->group_name;
+ $arguments = unserialize($row->arguments);
+ $arguments['field_mappings'] = array();
+ $arguments['group_name'] = $group_name;
+ $field_mappings = array();
+ $default_values = array();
+ $issue_group_values = array();
+
+ $migration = Migration::getInstance($machine_name);
+ if (is_a($migration, 'Migration')) {
+ $existing_mappings = $migration->getFieldMappings();
+ $coded_mappings = $migration->getCodedFieldMappings();
+ foreach ($form_state['values']['field_mappings'] as $destination_field => $info) {
+ // Treat an empty string for the default value as NULL.
+ if ($info['default_value'] === '') {
+ $info['default_value'] = NULL;
+ }
+
+ // If this mapping matches a coded mapping but not a stored mapping, remove
+ // it entirely (don't store it in the database) so the coded mapping is not
+ // overwritten.
+ if (isset($coded_mappings[$destination_field])) {
+ $coded_source_field =
+ $coded_mappings[$destination_field]->getSourceField();
+ $coded_default_value =
+ $coded_mappings[$destination_field]->getDefaultValue();
+ $coded_source_migration =
+ $coded_mappings[$destination_field]->getSourceMigration();
+ if ($info['mapping'] == '-1') {
+ $info['mapping'] = NULL;
+ }
+ if ($info['source_migration'] == '-1') {
+ $info['source_migration'] = NULL;
+ }
+ if ($info['issue_group'] == 0 && $coded_mappings[$destination_field]->getIssueGroup() != t('DNM') ||
+ $info['issue_group'] == 1 && $coded_mappings[$destination_field]->getIssueGroup() == t('DNM')) {
+ $dnm_matches = TRUE;
+ }
+ else {
+ $dnm_matches = FALSE;
+ }
+ if ($info['mapping'] == $coded_source_field &&
+ $info['default_value'] == $coded_default_value &&
+ $info['source_migration'] == $coded_source_migration &&
+ $dnm_matches) {
+ continue;
+ }
+ }
+ // This is not a match for a coded mapping, so we will want to save it.
+ $field_mappings[$destination_field] = $info['mapping'];
+ $default_values[$destination_field] = $info['default_value'];
+ $source_migrations[$destination_field] = $info['source_migration'];
+ $issue_group_values[$destination_field] = $info['issue_group'];
+ }
+
+ foreach ($field_mappings as $destination_field => $source_field) {
+ if ($source_field == -1) {
+ $source_field = NULL;
+ }
+
+ if ($issue_group_values[$destination_field]) {
+ $source_field = NULL;
+ $default = NULL;
+ }
+ else {
+ $default = $default_values[$destination_field];
+ }
+
+ // Until we can provide editing of all the options that go along with
+ // field mappings, we want to avoid overwriting pre-existing mappings and
+ // losing important bits.
+ $mapping = NULL;
+ if (isset($existing_mappings[$destination_field]) &&
+ $issue_group_values[$destination_field] != 0) {
+ $old_mapping = $existing_mappings[$destination_field];
+ if ($source_field == $old_mapping->getSourceField() &&
+ $default_values[$destination_field] == $old_mapping->getDefaultValue() &&
+ $source_migrations[$destination_field] == $old_mapping->getSourceMigration()) {
+ // First, if this mapping matches a previously-stored mapping, we want to
+ // preserve it as it was originally stored.
+ if ($old_mapping->getMappingSource() ==
+ MigrateFieldMapping::MAPPING_SOURCE_DB) {
+ $mapping = $old_mapping;
+ }
+ // If it matches a coded mapping, then we don't want to save it at all.
+ else {
+ continue;
+ }
+ }
+ }
+
+ // We're not skipping this mapping, or preserving an old one, so create the
+ // new mapping.
+ if (!$mapping) {
+ $mapping = new MigrateFieldMapping($destination_field, $source_field);
+ $mapping->defaultValue($default);
+ }
+
+ if ($issue_group_values[$destination_field]) {
+ $mapping->issueGroup(t('DNM'));
+ }
+ else {
+ $mapping->issueGroup(NULL);
+ }
+
+ if ($source_migrations[$destination_field] &&
+ $source_migrations[$destination_field] != '-1') {
+ $mapping->sourceMigration($source_migrations[$destination_field]);
+ }
+
+ $arguments['field_mappings'][] = $mapping;
+ }
+
+ // Handle any source fields marked DNM.
+ foreach ($form_state['values']['source_fields'] as $source_field => $value) {
+ // Is this field ignored in the code?
+ $code_ignored = FALSE;
+ foreach ($coded_mappings as $destination_field => $mapping) {
+ if (is_numeric($destination_field) &&
+ $mapping->getSourceField() == $source_field) {
+ $code_ignored = TRUE;
+ }
+ }
+ // If it is marked DNM in the UI, but is not ignored in the code,
+ // generate a DNM mapping.
+ if ($value && !$code_ignored) {
+ $mapping = new MigrateFieldMapping(NULL, $source_field);
+ $mapping->issueGroup(t('DNM'));
+ $arguments['field_mappings'][] = $mapping;
+ }
+ // If it is marked DNM in the code, but is unchecked in the UI, see if
+ // it's mapped to a destination field through the UI. If not, generate
+ // an empty mapping without the DNM so it's available to be mapped.
+ elseif (!$value && $code_ignored) {
+ $mapping_found = FALSE;
+ foreach ($field_mappings as $destination_field => $mapped_source_field) {
+ if ($source_field == $mapped_source_field) {
+ $mapping_found = TRUE;
+ break;
+ }
+ }
+ if (!$mapping_found) {
+ $mapping = new MigrateFieldMapping(NULL, $source_field);
+ $arguments['field_mappings'][] = $mapping;
+ }
+ }
+ }
+ }
+
+ $arguments['dependencies'] = $arguments['soft_dependencies'] = array();
+ if (isset($form_state['values']['dependencies'])) {
+ foreach ($form_state['values']['dependencies'] as $dependency => $value) {
+ if ($value == 1) {
+ $arguments['dependencies'][] = $dependency;
+ }
+ elseif ($value == 2) {
+ $arguments['soft_dependencies'][] = $dependency;
+ }
+ }
+ }
+
+ Migration::registerMigration($class_name, $machine_name, $arguments);
+
+ drupal_set_message(t('Migration changes applied.'));
+ $form_state['redirect'] =
+ "admin/content/migrate/groups/$group_name/$machine_name";
+}
+
+/**
+ * Revert callback for the edit mappings form. Remove any field mappings that
+ * were defined through the UI.
+ *
+ * @param $form
+ * @param $form_state
+ */
+function migrate_ui_edit_mappings_revert(&$form, &$form_state) {
+ $machine_name = $form_state['values']['machine_name'];
+ db_delete('migrate_field_mapping')
+ ->condition('machine_name', $machine_name)
+ ->execute();
+ // Note that not all field mappings in the database came from the UI - they
+ // may also have been passed through a hook_migrate_api registration, so
+ // reregister the migration to restore those mappings.
+ migrate_static_registration(array($machine_name));
+}
+
+/**
+ * Theme function to layout field mappings in a table.
+ *
+ * @param array $variables
+ *
+ * @return string
+ * Rendered markup.
+ */
+function theme_migrate_ui_field_mapping_form($variables) {
+ $output = '';
+ $form = $variables['field_mappings'];
+ $elements = element_children($form);
+ if (!empty($elements)) {
+ $header = array(t('DNM'), t('Destination field'), t('Source field'),
+ t('Default value'), t('Source migration'));
+ $rows = array();
+ foreach ($elements as $mapping_key) {
+ $row = array();
+ $title = $form[$mapping_key]['mapping']['#title'];
+ unset($form[$mapping_key]['mapping']['#title']);
+ $row[] = drupal_render($form[$mapping_key]['issue_group']);
+ $row[] = $title;
+ $row[] = drupal_render($form[$mapping_key]['mapping']);
+ $row[] = drupal_render($form[$mapping_key]['default_value']);
+ $row[] = drupal_render($form[$mapping_key]['source_migration']);
+ $rows[] = $row;
+ }
+ $output .= theme('table', array('header' => $header, 'rows' => $rows));
+ }
+ $output .= drupal_render_children($form);
+ return $output;
+}
+
+/**
+ * Theme function to layout dependencies in a table.
+ *
+ * @param array $variables
+ *
+ * @return string
+ * Rendered markup.
+ */
+function theme_migrate_ui_field_mapping_dependencies($variables) {
+ $output = '';
+ $form = $variables['dependencies'];
+ $header = array(t('Migration'), t('Dependency'));
+ $rows = array();
+ $elements = element_children($form);
+ foreach ($elements as $mapping_key) {
+ $row = array();
+ $title = $form[$mapping_key]['#title'];
+ unset($form[$mapping_key]['#title']);
+ $row[] = $title;
+ $row[] = drupal_render($form[$mapping_key]);
+ $rows[] = $row;
+ }
+ $output .= theme('table', array('rows' => $rows, 'header' => $header, 'empty' => t('No other migrations were found.')));
+
+ $output .= drupal_render_children($form);
+ return $output;
+}
+
+/**
+ * Menu callback for messages page
+ */
+function migrate_ui_messages($group_name, $migration_name) {
+ drupal_set_title(t('Import messages for !migration',
+ array('!migration' => $migration_name)));
+
+ $build = $rows = array();
+
+ $migration = Migration::getInstance($migration_name);
+ if (!$migration) {
+ return $build;
+ }
+
+ $source_key = $migration->getMap()->getSourceKey();
+ $source_key_map = $migration->getMap()->getSourceKeyMap();
+ $source_key_map_flipped = array_flip($source_key_map);
+
+ $header = array();
+ // Add a table header for each source key in the migration's map.
+ foreach ($source_key as $key => $map_info) {
+ $header[] = array('data' => $map_info['description'], 'field' => $source_key_map[$key], 'sort' => 'asc');
+ }
+
+ $header[] = array('data' => t('Level'), 'field' => 'level');
+ $header[] = array('data' => t('Message'), 'field' => 'message');
+
+ // TODO: need a general MigrateMap API
+ $messages = $migration->getMap()->getConnection()
+ ->select($migration->getMap()->getMessageTable(), 'msg')
+ ->extend('PagerDefault')
+ ->extend('TableSort')
+ ->orderByHeader($header)
+ ->limit(500)
+ ->fields('msg')
+ ->execute();
+
+ foreach ($messages as $message) {
+ $classes[] = $message->level <= MigrationBase::MESSAGE_WARNING ? 'migrate-error' : '';
+ $row = array();
+ // Add a table column for each source key.
+ foreach ($source_key_map_flipped as $source_key => $source_field) {
+ $row[] = array(
+ 'data' => $message->{$source_key},
+ 'class' => $classes,
+ );
+ }
+ $row[] = array('data' => $migration->getMessageLevelName($message->level), 'class' => $classes);
+ $row[] = array('data' => $message->message, 'class' => $classes);
+
+ $rows[] = $row;
+
+ unset($classes);
+ }
+
+ $build['messages'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#empty' => t('No messages'),
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'migrate_ui') . '/migrate_ui.css'),
+ ),
+ );
+ $build['migrate_ui_pager'] = array('#theme' => 'pager');
return $build;
}
/**
- * Submit callback for the configuration form registration fieldset.
+ * Given a migration machine name, remove its tracking from the database.
+ *
+ * @param $machine_name
*/
-function migrate_ui_configure_register_submit($form, &$form_state) {
- migrate_autoregister();
- $form_state['redirect'] = 'admin/content/migrate';
-}
-
-/**
- * Submit callback for the configuration form auto-registration changes.
- */
-function migrate_ui_configure_register_auto_register($form, &$form_state) {
- variable_set('migrate_disable_autoregistration',
- !variable_get('migrate_disable_autoregistration', FALSE));
+function migrate_ui_deregister_migration($machine_name) {
+ // The class is gone, so we'll manually clear migrate_status, and make
+ // the default assumptions about the map/message tables.
+ db_drop_table('migrate_map_' . strtolower($machine_name));
+ db_drop_table('migrate_message_' . strtolower($machine_name));
+ db_delete('migrate_status')
+ ->condition('machine_name', $machine_name)
+ ->execute();
+ db_delete('migrate_field_mapping')
+ ->condition('machine_name', $machine_name)
+ ->execute();
+ drupal_set_message(t("Deregistered '!description' task",
+ array('!description' => $machine_name)));
}
/**
* Menu callback
*/
-function migrate_ui_handlers() {
- drupal_set_title(t('Migrate handler configuration'));
- return drupal_get_form('migrate_ui_handlers_form');
+function migrate_ui_configure() {
+ drupal_set_title(t('Configure migration settings'));
+ return drupal_get_form('migrate_ui_configure_form');
}
/**
* Form for reviewing migrations.
*/
-function migrate_ui_handlers_form($form, &$form_state) {
+function migrate_ui_configure_form($form, &$form_state) {
$build = array();
+ $description = t('To register (or reregister with updated arguments) any
+ migrations defined in hook_migrate_api(), click the Register
+ button below.');
+
+ $build['registration'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Registration'),
+ '#description' => $description,
+ );
+
+ $build['registration']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Register statically defined classes'),
+ '#submit' => array('migrate_ui_configure_register_submit'),
+ );
+
+ $migrations = array();
+ $result = db_select('migrate_status', 'ms')
+ ->fields('ms', array('class_name', 'machine_name'))
+ ->execute();
+ $migration_list = '';
+ foreach ($result as $row) {
+ if (!class_exists($row->class_name)) {
+ $migrations[] = $row->machine_name;
+ $migration_list .= '- ' . t('!migration (class !class)',
+ array('!migration' => $row->machine_name, '!class' => $row->class_name)) . "
\n";
+ }
+ }
+
+ if (!empty($migrations)) {
+ $description = t('No class currently exists for the following migrations: !list
',
+ array('!list' => $migration_list));
+ $build['deregistration'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Orphaned migration tasks'),
+ '#description' => $description,
+ );
+ $build['deregistration']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Deregister orphans'),
+ '#submit' => array('migrate_ui_configure_deregister_submit'),
+ );
+ }
+
+ $build['settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Settings'),
+ );
+ $build['settings']['deprecation_warnings'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Warn on usage of deprecated patterns'),
+ '#default_value' => variable_get('migrate_deprecation_warnings', 1),
+ '#description' => t('When checked, you will receive warnings when using methods and patterns that are deprecated as of Migrate 2.6.'),
+ );
+ $build['settings']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save settings'),
+ '#submit' => array('migrate_ui_configure_settings_submit'),
+ );
+
+ // Configure background imports if the drush command has been set.
+ $drush_path = trim(variable_get('migrate_drush_path', ''));
+ $drush_validated = FALSE;
+ if ($drush_path) {
+ // Try running a drush status command to verify it's properly configured.
+ $uri = $GLOBALS['base_url'];
+ $uid = $GLOBALS['user']->uid;
+ $command = "$drush_path status --user=$uid --uri=$uri --root=" . DRUPAL_ROOT;
+ exec($command, $output, $status);
+ if ($status == 0) {
+ $version = '';
+ foreach ($output as $line) {
+ if (preg_match('|Drush version +: +(.*)|', $line, $matches)) {
+ $version = trim($matches[1]);
+ }
+ elseif (preg_match('|Drupal bootstrap +: +Successful *|', $line, $matches)) {
+ $drush_validated = TRUE;
+ }
+ }
+ }
+ if ($drush_validated) {
+ $description = t('Configure the use of drush version @version to run import and rollback operations in the background.',
+ array(
+ '@drush' => 'http://drupal.org/project/drush',
+ '@version' => $version,
+ )
+ );
+ }
+ else {
+ $description = t('Drush is misconfigured on the server. Please review the documentation on drupal.org, and make sure everything is set up correctly.',
+ array(
+ '@drush' => 'http://drupal.org/project/drush',
+ '@config' => 'http://drupal.org/node/1958170',
+ )
+ );
+ }
+ }
+ else {
+ $description = t('To enable running operations in the background with drush, (which is recommended), some configuration must be done on the server. See the documentation on drupal.org.',
+ array(
+ '@drush' => 'http://drupal.org/project/drush',
+ '@recommended' => 'http://drupal.org/node/1806824',
+ '@config' => 'http://drupal.org/node/1958170',
+ '@dorg' => 'http://drupal.org/',
+ )
+ );
+ }
+
+ $build['drush'] = array(
+ '#type' => 'fieldset',
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#title' => t('Background operations'),
+ '#description' => $description,
+ );
+
+ if ($drush_validated) {
+ $options = array(
+ 0 => t('Immediate operation only'),
+ 1 => t('Background operation only'),
+ 2 => t('Allows both immediate and background operations'),
+ );
+ $build['drush']['migrate_import_method'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => variable_get('migrate_import_method', 0),
+ '#title' => t('Import method'),
+ '#description' => t('Choose whether the dashboard will perform operations immediately, in the background via drush, or offer a choice.'),
+ );
+
+ $build['drush']['migrate_drush_mail'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('migrate_drush_mail', 0),
+ '#title' => t('Send email notification when a background operation completes.'),
+ );
+
+ $build['drush']['migrate_drush_mail_subject'] = array(
+ '#type' => 'textfield',
+ '#default_value' => variable_get('migrate_drush_mail_subject',
+ t('Migration operation completed')),
+ '#title' => t('Subject'),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="migrate_drush_mail"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ $build['drush']['migrate_drush_mail_body'] = array(
+ '#type' => 'textarea',
+ '#rows' => 10,
+ '#default_value' => variable_get('migrate_drush_mail_body',
+ t('The migration operation is complete. Please review the results on your dashboard')),
+ '#title' => t('Body'),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="migrate_drush_mail"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ $build['drush']['save_drush_config'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save background configuration'),
+ '#submit' => array('migrate_ui_configure_drush_submit'),
+ );
+ }
+
$build['handlers'] = array(
'#type' => 'fieldset',
- '#title' => t('Handler configuration'),
- '#description' => t('In some cases, such as when a field handler for a contributed module is
- implemented in both migrate_extras and the module itself, you may need to disable
- a particular handler. In this case, you may uncheck the undesired handler below.'),
+ '#title' => t('Handlers'),
+ '#description' => t('In some cases, such as when a field handler for a contributed module is implemented in both migrate_extras and the module itself, you may need to disable a particular handler. In this case, you may uncheck the undesired handler below.'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
);
$build['handlers']['destination'] = array(
@@ -825,7 +1805,8 @@ function migrate_ui_handlers_form($form, &$form_state) {
'types' => array('data' => t('Destination types handled')),
);
- $disabled = unserialize(variable_get('migrate_disabled_handlers', serialize(array())));
+ $disabled = unserialize(variable_get('migrate_disabled_handlers',
+ serialize(array())));
$class_list = _migrate_class_list('MigrateDestinationHandler');
$rows = array();
$default_values = array();
@@ -891,16 +1872,60 @@ function migrate_ui_handlers_form($form, &$form_state) {
$build['handlers']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save handler statuses'),
- '#submit' => array('migrate_ui_handlers_submit'),
+ '#submit' => array('migrate_ui_configure_submit'),
);
return $build;
}
/**
- * Submit callback for the configuration form handler fieldset.
+ * Submit callback for the configuration form registration fieldset.
*/
-function migrate_ui_handlers_submit($form, &$form_state) {
+function migrate_ui_configure_register_submit($form, &$form_state) {
+ migrate_static_registration();
+ drupal_set_message(t('All statically defined migrations have been (re)registered.'));
+ $form_state['redirect'] = 'admin/content/migrate';
+}
+
+/**
+ * Submit callback for the configuration form deregistration fieldset.
+ */
+function migrate_ui_configure_deregister_submit($form, &$form_state) {
+ $result = db_select('migrate_status', 'ms')
+ ->fields('ms', array('class_name', 'machine_name'))
+ ->execute();
+ foreach ($result as $row) {
+ if (!class_exists($row->class_name)) {
+ migrate_ui_deregister_migration($row->machine_name);
+ }
+ }
+ $form_state['redirect'] = 'admin/content/migrate';
+}
+
+/**
+ * Submit callback for the configuration form settings fieldset.
+ */
+function migrate_ui_configure_settings_submit($form, &$form_state) {
+ variable_set('migrate_deprecation_warnings',
+ $form_state['values']['deprecation_warnings']);
+ drupal_set_message(t('Migration settings saved.'));
+}
+
+/**
+ * Submit callback for the drush configuration form handler fieldset.
+ */
+function migrate_ui_configure_drush_submit($form, &$form_state) {
+ $values = $form_state['values'];
+ variable_set('migrate_import_method', $values['migrate_import_method']);
+ variable_set('migrate_drush_mail', $values['migrate_drush_mail']);
+ variable_set('migrate_drush_mail_subject', $values['migrate_drush_mail_subject']);
+ variable_set('migrate_drush_mail_body', $values['migrate_drush_mail_body']);
+}
+
+/**
+ * Submit callback for the handler configuration form handler fieldset.
+ */
+function migrate_ui_configure_submit($form, &$form_state) {
$disabled = array();
foreach ($form_state['values']['destination_handlers'] as $class => $value) {
if (!$value) {
diff --git a/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.wizard.inc b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.wizard.inc
new file mode 100644
index 00000000..1ad88a58
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/migrate_ui/migrate_ui.wizard.inc
@@ -0,0 +1,663 @@
+ $info) {
+ // Add any extenders.
+ // @todo: consider allowing extender classes to declare dependencies on
+ // other extender classes, to ensure they work in the correct order?
+ if (isset($info['wizard extenders'])) {
+ foreach ($info['wizard extenders'] as $wizard_class => $extender_classes) {
+ // Note that $class_name is in lower case, so we can't just use isset()
+ // to find our wizard.
+ if (strtolower($wizard_class) == $class_name) {
+ foreach ($extender_classes as $extender_class) {
+ $wizard->addExtender($extender_class);
+ }
+ }
+ }
+ }
+ }
+
+ $form_state['wizard'] = $wizard;
+ }
+ else {
+ $wizard = $form_state['wizard'];
+ }
+
+ // Fetch the form for the wizard's current step.
+ $form = $wizard->form($form_state);
+
+ return $form;
+}
+
+/**
+ * Submit handler for the "previous" button. Moves the wizard back to the
+ * previous step, and retrieves the values that were submitted on that step.
+ *
+ * @todo: Can we remove steps that were dynamically added?
+ */
+function migrate_ui_wizard_previous_submit($form, &$form_state) {
+ /** @var MigrateUIWizard $wizard */
+ $wizard = $form_state['wizard'];
+ $wizard->gotoPreviousStep($form_state);
+}
+
+/**
+ * Validate handler for the 'next' button. Dispatches to the wizard's current
+ * step for validation.
+ */
+function migrate_ui_wizard_next_validate($form, &$form_state) {
+ /** @var MigrateUIWizard $wizard */
+ $wizard = $form_state['wizard'];
+ $wizard->formValidate($form_state);
+}
+
+/**
+ * Submit handler for the 'next' button. Saves the form values for the step
+ * we're leaving, so Previous can pick them up, and moves the wizard to the
+ * next step.
+ */
+function migrate_ui_wizard_next_submit($form, &$form_state) {
+ /** @var MigrateUIWizard $wizard */
+ $wizard = $form_state['wizard'];
+ $wizard->gotoNextStep($form_state);
+}
+
+/**
+ * Submit handler for the Save settings button. Register the migrations that were
+ * (implicitly) defined along the way and redirect to the Migrate dashboard.
+ */
+function migrate_ui_wizard_submit($form, &$form_state) {
+ /** @var MigrateUIWizard $wizard */
+ $wizard = $form_state['wizard'];
+ $wizard->formSaveSettings();
+ $form_state['redirect'] = 'admin/content/migrate/groups/' .
+ $wizard->getGroupName();
+}
+
+/**
+ * Submit handler for the "Save settings and import" button. Register the
+ * migrations that were (implicitly) defined along the way, run the import, and
+ * redirect to the Migrate dashboard.
+ */
+function migrate_ui_wizard_migrate_submit($form, &$form_state) {
+ /** @var MigrateUIWizard $wizard */
+ $wizard = $form_state['wizard'];
+ $wizard->formSaveSettings();
+ $wizard->formPerformImport();
+ $form_state['redirect'] = 'admin/content/migrate/groups/' .
+ $wizard->getGroupName();
+}
+
+/**
+ * The base class for migration wizards. Extend this class to implement a
+ * wizard UI for importing into Drupal from a given source format (Drupal,
+ * WordPress, etc.).
+ */
+abstract class MigrateUIWizard {
+ /**
+ * We maintain a doubly-linked list of wizard steps, both to support
+ * previous/next, and to easily insert steps dynamically.
+ *
+ * The first step of the wizard, which has no predecessor. Will generally be
+ * an overview/introductory page.
+ *
+ * @var MigrateUIStep
+ */
+ protected $firstStep;
+
+ /**
+ * The last step of the wizard, which has no successor. Will generally be a
+ * review page.
+ *
+ * @var MigrateUIStep
+ */
+ protected $lastStep;
+
+ /**
+ * Get the list of steps currently defined.
+ *
+ * @return
+ * An array of MigrateUIStep objects, in the order defined, keyed by the step
+ * name.
+ */
+ protected function getSteps() {
+ $steps = array();
+
+ $steps[$this->firstStep->getName()] = $this->firstStep;
+ $next_step = $this->firstStep->nextStep;
+ while (!is_null($next_step)) {
+ $steps[$next_step->getName()] = $next_step;
+
+ $next_step = $next_step->nextStep;
+ }
+
+ return $steps;
+ }
+
+ /**
+ * The current step of the wizard (the one being shown in the UI, and the one
+ * whose button is being clicked on).
+ *
+ * @var MigrateUIStep
+ */
+ protected $currentStep;
+
+ /**
+ * The step number, used in the page title.
+ *
+ * @var int
+ */
+ protected $stepNumber = 1;
+
+ /**
+ * The group name to assign to any Migration instances created.
+ *
+ * @var string
+ */
+ protected $groupName = 'default';
+ public function getGroupName() {
+ return $this->groupName;
+ }
+
+ /**
+ * The user-visible title of the group.
+ *
+ * @var string
+ */
+ protected $groupTitle = 'default';
+
+ /**
+ * Any arguments that apply to all migrations in the group.
+ *
+ * @var array
+ */
+ protected $groupArguments = array();
+
+ /**
+ * Array of Migration argument arrays, keyed by machine name. On Finish, used
+ * to register Migrations.
+ *
+ * @var array
+ */
+ protected $migrations = array();
+
+ /**
+ * Array of MigrateUIWizardExtender objects that extend this wizard.
+ *
+ * @var array
+ */
+ protected $extenders = array();
+ public function getExtender($extender_class) {
+ if (isset($this->extenders[$extender_class])) {
+ return $this->extenders[$extender_class];
+ }
+ else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Returns the translatable name representing the source of the data (e.g.,
+ * "Drupal", "WordPress", etc.).
+ *
+ * @return string
+ */
+ abstract public function getSourceName();
+
+ public function __construct() {}
+
+ /**
+ * Add a wizard extender.
+ *
+ * This initializes the new extender and adds it to our internal list.
+ *
+ * @param $extender_class
+ * The name of an extender class.
+ */
+ public function addExtender($extender_class) {
+ $steps = $this->getSteps();
+
+ $extender = new $extender_class($this, $steps);
+ $this->extenders[$extender_class] = $extender;
+ }
+
+ /**
+ * Add a step to the wizard, using a step name and method.
+ *
+ * @param string $name
+ * Translatable name for the step, to be used in the page title.
+ * @param callable $form_method
+ * Callable returning the form array for the step. This can be either the
+ * name of a MigrateUIWizard method, or a callable array specifying a method
+ * on a wizard extender. The validation method is formed from the method's
+ * name with the suffix 'Validate' added.
+ * @param MigrateUIStep $after
+ * Optional step after which to insert the new step. If omitted, add it at
+ * the end.
+ * @param mixed $context
+ * Optional data to be used by this step's form.
+ *
+ * @return MigrateUIStep
+ */
+ public function addStep($name, $form_method, MigrateUIStep $after = NULL, $context = NULL) {
+ if (!is_array($form_method)) {
+ $form_method = array($this, $form_method);
+ }
+
+ $new_step = new MigrateUIStep($name, $form_method, $context);
+
+ // There were no steps, so this is the only one.
+ if (is_null($this->firstStep)) {
+ $this->firstStep = $this->lastStep = $this->currentStep = $new_step;
+ }
+ else {
+ // If no insertion point is specified, append to the end.
+ if (is_null($after)) {
+ $after = $this->lastStep;
+ }
+ // Do the insert, rewriting the links appropriately.
+ $new_step->nextStep = $after->nextStep;
+
+ if (is_null($new_step->nextStep)) {
+ $this->lastStep = $new_step;
+ }
+ else {
+ $new_step->nextStep->previousStep = $new_step;
+ }
+ $new_step->previousStep = $after;
+ $after->nextStep = $new_step;
+ }
+ return $new_step;
+ }
+
+ /**
+ * Remove the named step from the wizard.
+ *
+ * @param $name
+ */
+ protected function removeStep($name) {
+ for ($current_step = $this->firstStep; !is_null($current_step); $current_step = $current_step->nextStep) {
+ if ($current_step->getName() == $name) {
+ if (is_null($current_step->previousStep)) {
+ $this->firstStep = $current_step->nextStep;
+ }
+ else {
+ $current_step->previousStep->nextStep = $current_step->nextStep;
+ }
+ if (is_null($current_step->nextStep)) {
+ $this->lastStep = $current_step->previousStep;
+ }
+ else {
+ $current_step->nextStep->previousStep = $current_step->previousStep;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Move the wizard to the next step in line (if any), first squirreling away
+ * the current step's form values.
+ */
+ public function gotoNextStep(&$form_state) {
+ if ($this->currentStep && $this->currentStep->nextStep) {
+ $this->currentStep->setFormValues($form_state['values']);
+ $form_state['rebuild'] = TRUE;
+ $this->currentStep = $this->currentStep->nextStep;
+ $this->stepNumber++;
+
+ // Ensure a page reload remains on the current step.
+ $current_step_form_values = $this->currentStep->getFormValues();
+ if (!empty($current_step_form_values)) {
+ $form_state['values'] = $current_step_form_values;
+ }
+ else {
+ $form_state['values'] = array();
+ }
+ }
+ }
+
+ /**
+ * Move the wizard to the previous step in line (if any), restoring its
+ * form values.
+ */
+ public function gotoPreviousStep(&$form_state) {
+ if ($this->currentStep && $this->currentStep->previousStep) {
+ $this->currentStep = $this->currentStep->previousStep;
+ $this->stepNumber--;
+ $form_state['values'] = $this->currentStep->getFormValues();
+ $form_state['rebuild'] = TRUE;
+ }
+ }
+
+ /**
+ * Build the form for the current step.
+ *
+ * @return array
+ */
+ public function form(&$form_state) {
+ drupal_set_title(t('Import from @source_title',
+ array('@source_title' => $this->getSourceName())));
+
+ $form_method = $this->currentStep->getFormMethod();
+ $form['title'] = array(
+ '#prefix' => '',
+ '#markup' => t('Step @step: @step_name',
+ array(
+ '@step' => $this->stepNumber,
+ '@step_name' => $this->currentStep->getName())),
+ '#suffix' => '
',
+ );
+
+ $form += call_user_func($form_method, $form_state);
+
+ $form['actions'] = array('#type' => 'actions');
+
+ // Show the 'previous' button if appropriate. Note that #submit is set to
+ // a special submit handler, and that we use #limit_validation_errors to
+ // skip all complaints about validation when using the back button. The
+ // values entered will be discarded, but they will not be validated, which
+ // would be annoying in a "back" button.
+ if ($this->currentStep != $this->firstStep) {
+ $form['actions']['prev'] = array(
+ '#type' => 'submit',
+ '#value' => t('Previous'),
+ '#name' => 'prev',
+ '#submit' => array('migrate_ui_wizard_previous_submit'),
+ '#limit_validation_errors' => array(),
+ );
+ }
+
+ // Show the Next button only if there are more steps defined.
+ if ($this->currentStep == $this->lastStep) {
+ $form['actions']['finish'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save import settings'),
+ );
+
+ $form['actions']['migrate'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save import settings and run import'),
+ '#submit' => array('migrate_ui_wizard_migrate_submit'),
+ );
+ }
+ else {
+ $form['actions']['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Next'),
+ '#name' => 'next',
+ '#submit' => array('migrate_ui_wizard_next_submit'),
+ '#validate' => array('migrate_ui_wizard_next_validate'),
+ );
+ }
+ return $form;
+ }
+
+ /**
+ * Call the validation function for the current form (which has the same
+ * name of the form function with 'Validate' appended).
+ *
+ * @param array $form_state
+ */
+ public function formValidate(&$form_state) {
+ $validate_method = $this->currentStep->getFormMethod();
+
+ // This is an array for a method, or a function name.
+ if (is_array($validate_method)) {
+ $validate_method[1] .= 'Validate';
+ }
+ else {
+ $validate_method .= 'Validate';
+ }
+
+ if (is_callable($validate_method)) {
+ call_user_func($validate_method, $form_state);
+ }
+ }
+
+ /**
+ * Take the information we've accumulated throughout the wizard, and create
+ * the Migrations to perform the import.
+ */
+ public function formSaveSettings() {
+ MigrateGroup::register($this->groupName, $this->groupTitle, $this->groupArguments);
+ $info['arguments']['group_name'] = $this->groupName;
+ foreach ($this->migrations as $machine_name => $info) {
+ // Call the right registerMigration implementation. Note that this means
+ // that classes that override registerMigration() must handle registration
+ // themselves, they cannot leave it to us and expect their extension to be
+ // called.
+ if (is_subclass_of($info['class_name'], 'Migration')) {
+ Migration::registerMigration($info['class_name'], $machine_name,
+ $info['arguments']);
+ }
+ else {
+ MigrationBase::registerMigration($info['class_name'], $machine_name,
+ $info['arguments']);
+ }
+ };
+ menu_rebuild();
+ }
+
+ /**
+ * Run the import process for the migration group we've defined.
+ */
+ public function formPerformImport() {
+ $migrations = migrate_migrations();
+ $operations = array();
+ /** @var Migration $migration */
+ foreach ($migrations as $migration) {
+ $group_name = $migration->getGroup()->getName();
+ if ($group_name == $this->groupName) {
+ $operations[] = array('migrate_ui_batch', array('import', $migration->getMachineName(), NULL, 0));
+ }
+ }
+
+ if (count($operations) > 0) {
+ $batch = array(
+ 'operations' => $operations,
+ 'title' => t('Import processing'),
+ 'file' => drupal_get_path('module', 'migrate_ui') . '/migrate_ui.pages.inc',
+ 'init_message' => t('Starting import process'),
+ 'progress_message' => t(''),
+ 'error_message' => t('An error occurred. Some or all of the import processing has failed.'),
+ 'finished' => 'migrate_ui_batch_finish',
+ );
+ batch_set($batch);
+ }
+ }
+
+ /**
+ * Record all the information necessary to register a migration when this is
+ * all over.
+ *
+ * @param string $machine_name
+ * Machine name for the migration class.
+ * @param string $class_name
+ * Name of the Migration class to instantiate.
+ * @param array $arguments
+ * Further information configuring the migration.
+ */
+ public function addMigration($machine_name, $class_name, $arguments) {
+ // Give extenders an opportunity to modify or reject this migration.
+ foreach ($this->extenders as $extender) {
+ if (!$extender->addMigrationAlter($machine_name, $class_name, $arguments, $this)) {
+ return FALSE;
+ }
+ }
+
+ $machine_name = $this->groupName . $machine_name;
+ if (isset($arguments['dependencies'])) {
+ foreach ($arguments['dependencies'] as $index => $dependency) {
+ $arguments['dependencies'][$index] = $this->groupName . $dependency;
+ }
+ }
+ if (isset($arguments['soft_dependencies'])) {
+ foreach ($arguments['soft_dependencies'] as $index => $dependency) {
+ $arguments['soft_dependencies'][$index] = $this->groupName . $dependency;
+ }
+ }
+ $arguments += array(
+ 'group_name' => $this->groupName,
+ 'machine_name' => $machine_name,
+ );
+ $this->migrations[$machine_name] = array(
+ 'class_name' => $class_name,
+ 'arguments' => $arguments,
+ );
+
+ return TRUE;
+ }
+}
+
+/**
+ * Class representing one step of a wizard.
+ */
+class MigrateUIStep {
+ /**
+ * A translatable string briefly describing this step, to be used in the page
+ * title for the step form.
+ *
+ * @var string
+ */
+ protected $name;
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Callable that returns the form array for this step.
+ *
+ * @var string
+ */
+ protected $formMethod;
+ public function getFormMethod() {
+ return $this->formMethod;
+ }
+
+ /**
+ * The form values ($form_state['values']) submitted for this step, saved in
+ * case we need to restore them on a Previous action.
+ *
+ * @var array
+ */
+ protected $formValues;
+ public function getFormValues() {
+ return $this->formValues;
+ }
+ public function setFormValues($form_values) {
+ $this->formValues = $form_values;
+ }
+
+ /**
+ * Any contextual data needed by the form for this step. For example, a
+ * field mapping form would need to know the source and destination content
+ * types so it can determine what fields to expose.
+ *
+ * @var mixed
+ */
+ protected $context;
+ public function getContext() {
+ return $this->context;
+ }
+
+ /**
+ * The step object is a node in a doubly-linked list - it links to its
+ * predecessor and successor steps.
+ *
+ * @var MigrateUIStep
+ */
+ public $nextStep;
+
+ /**
+ * @var MigrateUIStep
+ */
+ public $previousStep;
+
+ /**
+ * Class constructor.
+ *
+ * @param $name
+ * The machine name of the wizard step.
+ * @param $form_method
+ * A callable for the form array for this step. The validation method is
+ * formed from the method name with the suffix 'Validate' added, regardless
+ * of which object it is on.
+ * @param $context = NULL
+ * Contextual data needed by the form for this step.
+ */
+ public function __construct($name, $form_method, $context = NULL) {
+ $this->name = $name;
+ $this->formMethod = $form_method;
+ $this->context = $context;
+ }
+}
+
+/**
+ *
+ */
+abstract class MigrateUIWizardExtender {
+ /**
+ * Reference to the wizard object that this extender applies to.
+ */
+ protected $wizard;
+
+ /**
+ * Class constructor.
+ *
+ * Wizard extenders should override this to add their steps to the wizard.
+ */
+ public function __construct(MigrateUIWizard $wizard, array $wizard_steps) {
+ $this->wizard = $wizard;
+ }
+
+ /**
+ * Alter the arguments to a migration before it is registered, or potentially
+ * reject it.
+ *
+ * @param string $machine_name
+ * Machine name for the migration class.
+ * @param string $class_name
+ * Name of the Migration class to instantiate.
+ * @param array $arguments
+ * Further information configuring the migration.
+ * @param MigrateUIWizard $wizard
+ * The wizard class performing the registration.
+ *
+ * @return bool
+ * Return FALSE to prevent registration of this migration.
+ */
+ public function addMigrationAlter($machine_name, $class_name, &$arguments, $wizard) {
+ return TRUE;
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/block_custom.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/block_custom.inc
new file mode 100644
index 00000000..75342a06
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/block_custom.inc
@@ -0,0 +1,237 @@
+ array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'ID of destination custom block',
+ ),
+ );
+ }
+ public function __construct() {
+ parent::__construct();
+ }
+
+ public function __toString() {
+ $output = t('Custom blocks');
+ return $output;
+ }
+
+ /**
+ * Returns a list of fields available to be mapped for custom blocks.
+ *
+ * @param Migration $migration
+ * Optionally, the migration containing this destination.
+ * @return array
+ * Keys: machine names of the fields (to be passed to addFieldMapping)
+ * Values: Human-friendly descriptions of the fields.
+ */
+ public function fields($migration = NULL) {
+ $fields = array(
+ 'bid' => t('The custom block ID (bid).'),
+ 'body' => t('Block contents.'),
+ 'info' => t('Block description.'),
+ 'format' => t('The {filter_format}.format of the block body.'),
+ );
+ return $fields;
+ }
+
+ /**
+ * Import a single row.
+ *
+ * @param $block
+ * Custom block object to build. Prefilled with any fields mapped in the Migration.
+ * @param $row
+ * Raw source data object - passed through to prepare/complete handlers.
+ * @return array
+ * Array of key fields of the object that was saved if
+ * successful. FALSE on failure.
+ */
+ public function import(stdClass $block, stdClass $row) {
+ // Updating previously-migrated content
+ if (isset($row->migrate_map_destid1)) {
+ $block->bid = $row->migrate_map_destid1;
+ }
+
+ // Load old values if necessary.
+ $migration = Migration::currentMigration();
+ if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
+ if (!isset($block->bid)) {
+ throw new MigrateException(t('System-of-record is DESTINATION, but no destination bid provided'));
+ }
+ if (!$old_block = $this->loadCustomBlock($block->bid)) {
+ throw new MigrateException(t('System-of-record is DESTINATION, and the provided bid could not be found'));
+ }
+ $block_to_update = (object) $old_block;
+ foreach ($old_block as $key => $value) {
+ if (!isset($block->$key)) {
+ $block->$key = $old_block[$key];
+ }
+ }
+ }
+
+ // Invoke migration prepare handlers
+ $this->prepare($block, $row);
+
+ // Custom blocks are handled as arrays, so clone the object to an array.
+ $item = clone $block;
+ $item = (array) $item;
+
+ migrate_instrument_start('block_custom_save');
+
+ // Check to see if this is a new custom block.
+ $update = FALSE;
+ if (isset($item['bid'])) {
+ $update = TRUE;
+ $bid = $this->saveCustomBlock($item);
+ }
+ else {
+ $bid = $this->saveCustomBlock($item);
+ }
+ migrate_instrument_stop('block_custom_save');
+
+ // Return the new id or FALSE on failure.
+ if (!empty($bid)) {
+ // Increment the count if the save succeeded.
+ if ($update) {
+ $this->numUpdated++;
+ }
+ else {
+ $this->numCreated++;
+ }
+ // Return the primary key to the mapping table.
+ $return = array($bid);
+ }
+ else {
+ $return = FALSE;
+ }
+
+ // Invoke migration complete handlers.
+ $block = (object) $this->loadCustomBlock($bid);
+ $this->complete($block, $row);
+
+ return $return;
+ }
+
+ /**
+ * Implementation of MigrateDestination::prepare().
+ */
+ public function prepare($block, stdClass $row) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ $block->migrate = array(
+ 'machineName' => $migration->getMachineName(),
+ );
+
+ // Call any general handlers.
+ migrate_handler_invoke_all('block_custom', 'prepare', $block, $row);
+ // Then call any prepare handler for this specific Migration.
+ if (method_exists($migration, 'prepare')) {
+ $migration->prepare($block, $row);
+ }
+ }
+
+ /**
+ * Implementation of MigrateDestination::complete().
+ */
+ public function complete($block, stdClass $row) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ $block->migrate = array(
+ 'machineName' => $migration->getMachineName(),
+ );
+ // Call any general handlers.
+ migrate_handler_invoke_all('block_custom', 'complete', $block, $row);
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'complete')) {
+ $migration->complete($block, $row);
+ }
+ }
+
+ /**
+ * Delete a batch of custom blocks at once.
+ *
+ * @param $bids
+ * Array of custom block IDs to be deleted.
+ */
+ public function bulkRollback(array $bids) {
+ migrate_instrument_start('block_custom_delete_multiple');
+ $this->prepareRollback($bids);
+ $this->deleteMultipleCustomBlocks($bids);
+ $this->completeRollback($bids);
+ migrate_instrument_stop('block_custom_delete_multiple');
+ }
+
+ /**
+ * Give handlers a shot at cleaning up before a block has been rolled back.
+ *
+ * @param $bid
+ * ID of the custom block about to be deleted.
+ */
+ public function prepareRollback($bid) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ // Call any general handlers.
+ migrate_handler_invoke_all('block_custom', 'prepareRollback', $bid);
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'prepareRollback')) {
+ $migration->prepareRollback($bid);
+ }
+ }
+
+ /**
+ * Give handlers a shot at cleaning up after a block has been rolled back.
+ *
+ * @param $bid
+ * ID of the custom block which has been deleted.
+ */
+ public function completeRollback($bid) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ // Call any general handlers.
+ migrate_handler_invoke_all('block_custom', 'completeRollback', $bid);
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'completeRollback')) {
+ $migration->completeRollback($bid);
+ }
+ }
+
+ public function loadCustomBlock($bid) {
+ return block_custom_block_get($bid);
+ }
+
+ public function saveCustomBlock($block) {
+ drupal_alter('block_custom', $block);
+ if (!empty($block['bid'])) {
+ drupal_write_record('block_custom', $block, array('bid'));
+ module_invoke_all('block_custom_update', $block);
+ }
+ else {
+ drupal_write_record('block_custom', $block);
+ module_invoke_all('block_custom_insert', $block);
+ }
+ return $block['bid'];
+ }
+
+ public function deleteCustomBlock($bid) {
+ $this->deleteMultipleCustomBlocks(array($bid));
+ }
+
+ public function deleteMultipleCustomBlocks(array $bids) {
+ db_delete('block_custom')->condition('bid', $bids, 'IN')->execute();
+ db_delete('block')->condition('module', 'block')->condition('delta', $bids, 'IN')->execute();
+ db_delete('block_role')->condition('module', 'block')->condition('delta', $bids, 'IN')->execute();
+ db_delete('block_node_type')->condition('module', 'block')->condition('delta', $bids, 'IN')->execute();
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/comment.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/comment.inc
index 48a0a906..caca29ab 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/comment.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/comment.inc
@@ -65,33 +65,33 @@ class MigrateDestinationComment extends MigrateDestinationEntity {
public function fields($migration = NULL) {
$fields = array();
// First the core (comment table) properties
- $fields['cid'] = t('Comment: Existing comment ID',
+ $fields['cid'] = t('Existing comment ID',
array('@doc' => 'http://drupal.org/node/1349714#cid'));
- $fields['nid'] = t('Comment: Node (by Drupal ID)',
+ $fields['nid'] = t('Node (by Drupal ID)',
array('@doc' => 'http://drupal.org/node/1349714#nid'));
- $fields['uid'] = t('Comment: User (by Drupal ID)',
+ $fields['uid'] = t('User (by Drupal ID)',
array('@doc' => 'http://drupal.org/node/1349714#uid'));
- $fields['pid'] = t('Comment: Parent (by Drupal ID)',
+ $fields['pid'] = t('Parent (by Drupal ID)',
array('@doc' => 'http://drupal.org/node/1349714#pid'));
- $fields['subject'] = t('Comment: Subject',
+ $fields['subject'] = t('Subject',
array('@doc' => 'http://drupal.org/node/1349714#subject'));
- $fields['created'] = t('Comment: Created timestamp',
+ $fields['created'] = t('Created timestamp',
array('@doc' => 'http://drupal.org/node/1349714#created'));
- $fields['changed'] = t('Comment: Modified timestamp',
+ $fields['changed'] = t('Modified timestamp',
array('@doc' => 'http://drupal.org/node/1349714#changed'));
- $fields['status'] = t('Comment: Status',
+ $fields['status'] = t('Status',
array('@doc' => 'http://drupal.org/node/1349714#status'));
- $fields['hostname'] = t('Comment: Hostname/IP address',
+ $fields['hostname'] = t('Hostname/IP address',
array('@doc' => 'http://drupal.org/node/1349714#hostname'));
- $fields['name'] = t('Comment: User name (not username)',
+ $fields['name'] = t('User name (not username)',
array('@doc' => 'http://drupal.org/node/1349714#name'));
- $fields['mail'] = t('Comment: Email address',
+ $fields['mail'] = t('Email address',
array('@doc' => 'http://drupal.org/node/1349714#mail'));
- $fields['homepage'] = t('Comment: Homepage',
+ $fields['homepage'] = t('Homepage',
array('@doc' => 'http://drupal.org/node/1349714#homepage'));
- $fields['language'] = t('Comment: Language',
+ $fields['language'] = t('Language',
array('@doc' => 'http://drupal.org/node/1349714#language'));
- $fields['thread'] = t('Comment: Thread',
+ $fields['thread'] = t('Thread',
array('@doc' => 'http://drupal.org/node/1349714#thread'));
// Then add in anything provided by handlers
@@ -149,6 +149,10 @@ class MigrateDestinationComment extends MigrateDestinationEntity {
$comment->changed = MigrationBase::timestamp($comment->changed);
}
+ if (!isset($comment->node_type)) {
+ $comment->node_type = $this->bundle;
+ }
+
if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
if (!isset($comment->cid)) {
throw new MigrateException(t('System-of-record is DESTINATION, but no destination cid provided'));
@@ -210,6 +214,10 @@ class MigrateDestinationComment extends MigrateDestinationEntity {
else {
$updating = FALSE;
}
+
+ // Validate field data prior to saving.
+ field_attach_validate('comment', $comment);
+
migrate_instrument_start('comment_save');
comment_save($comment);
migrate_instrument_stop('comment_save');
@@ -291,16 +299,48 @@ class MigrateDestinationComment extends MigrateDestinationEntity {
// Empty table
db_truncate('node_comment_statistics')->execute();
- // TODO: DBTNG. Ignore keyword is Mysql only? Is only used in the rare case when
- // two comments on the same node share same timestamp.
- $sql = "
- INSERT IGNORE INTO {node_comment_statistics} (nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) (
- SELECT c.nid, c.cid, c.created, c.name, c.uid, c2.comment_count FROM {comment} c
- JOIN (
- SELECT c.nid, MAX(c.created) AS created, COUNT(*) AS comment_count FROM {comment} c WHERE status=:published GROUP BY c.nid
- ) AS c2 ON c.nid = c2.nid AND c.created=c2.created
- )";
- db_query($sql, array(':published' => COMMENT_PUBLISHED));
+ // DBTNG. IGNORE keyword is not compatible with Postgres. SQLite?
+ switch (db_driver()) {
+ case 'pgsql':
+ // We still want to run this under Postgres. On the very rare occasion
+ // when we have 2 comments on the same node with the same timestamp
+ // we will lose data.
+ $sql = "
+ INSERT INTO {node_comment_statistics} (nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) (
+ SELECT c.nid, c.cid, c.created, c.name, c.uid, c2.comment_count
+ FROM {comment} c
+ JOIN (
+ SELECT c.nid, MAX(c.created) AS created, COUNT(*) AS comment_count
+ FROM {comment} c
+ WHERE status=:published
+ GROUP BY c.nid
+ ) AS c2 ON c.nid = c2.nid AND c.created=c2.created
+ )";
+ break;
+
+ default:
+ $sql = "
+ INSERT IGNORE INTO {node_comment_statistics} (nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) (
+ SELECT c.nid, c.cid, c.created, c.name, c.uid, c2.comment_count
+ FROM {comment} c
+ JOIN (
+ SELECT c.nid, MAX(c.created) AS created, COUNT(*) AS comment_count
+ FROM {comment} c
+ WHERE status=:published
+ GROUP BY c.nid
+ ) AS c2 ON c.nid = c2.nid AND c.created=c2.created
+ )";
+ }
+ try {
+ db_query($sql, array(':published' => COMMENT_PUBLISHED));
+ }
+ catch (Exception $e) {
+ // Our edge case has been hit. A Postgres migration has likely just
+ // lost data. Let the user know.
+ Migration::displayMessage(t('Failed to update node comment statistics: !message',
+ array('!message' => $e->getMessage())
+ ));
+ }
// Insert records into the node_comment_statistics for nodes that are missing.
$query = db_select('node', 'n');
@@ -324,7 +364,10 @@ class MigrateCommentNodeHandler extends MigrateDestinationHandler {
$this->registerTypes(array('node'));
}
- public function fields($entity_type, $bundle) {
+ /**
+ * Implementation of MigrateDestinationHandler::fields().
+ */
+ public function fields($entity_type, $bundle, $migration = NULL) {
$fields = array();
$fields['comment'] = t('Whether comments may be posted to the node');
return $fields;
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/entity.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/entity.inc
index f3c7ab6a..4e3540e3 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/entity.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/entity.inc
@@ -138,6 +138,11 @@ abstract class MigrateDestinationEntity extends MigrateDestination {
migrate_handler_invoke_all('Entity', 'prepare', $entity, $source_row);
// Then call any entity-specific handlers
migrate_handler_invoke_all($this->entityType, 'prepare', $entity, $source_row);
+
+ // Apply defaults, removing empty fields from the entity object.
+ $form = $form_state = array();
+ _field_invoke_default('submit', $this->entityType, $entity, $form, $form_state);
+
// Then call any prepare handler for this specific Migration.
if (method_exists($migration, 'prepare')) {
$migration->prepare($entity, $source_row);
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/fields.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/fields.inc
index 39eab7f9..69dd56e1 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/fields.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/fields.inc
@@ -12,15 +12,6 @@ class MigrateFieldsEntityHandler extends MigrateDestinationHandler {
/**
* Implementation of MigrateDestinationHandler::fields().
- *
- * @param $entity_type
- * The entity type (node, user, etc.) for which to list fields.
- * @param $bundle
- * The bundle (article, blog, etc.), if any, for which to list fields.
- * @param Migration $migration
- * Optionally, the migration providing the context.
- * @return array
- * An array keyed by field name, with field descriptions as values.
*/
public function fields($entity_type, $bundle, $migration = NULL) {
$fields = array();
@@ -29,12 +20,17 @@ class MigrateFieldsEntityHandler extends MigrateDestinationHandler {
$field_info = field_info_field($machine_name);
$type = $field_info['type'];
- $fields[$machine_name] = t('Field:') . ' ' . $instance['label'] .
- ' (' . $field_info['type'] . ')';
+ if (user_access(MIGRATE_ACCESS_ADVANCED)) {
+ $fields[$machine_name] = $instance['label'] . ' (' . $field_info['type'] . ')';
+ }
+ else {
+ $fields[$machine_name] = $instance['label'];
+ }
// Look for subfields
$class_list = _migrate_class_list('MigrateFieldHandler');
$disabled = unserialize(variable_get('migrate_disabled_handlers', serialize(array())));
+ $fields_found = FALSE;
foreach ($class_list as $class_name => $handler) {
if (!in_array($class_name, $disabled) && $handler->handlesType($type)
&& method_exists($handler, 'fields')) {
@@ -45,6 +41,18 @@ class MigrateFieldsEntityHandler extends MigrateDestinationHandler {
foreach ($subfields as $subfield_name => $subfield_label) {
$fields[$machine_name . ':' . $subfield_name] = $subfield_label;
}
+ $fields_found = TRUE;
+ }
+ }
+ if (!$fields_found) {
+ // Check the default field handler last.
+ migrate_instrument_start('MigrateDefaultFieldHandler->fields');
+ $subfields = call_user_func(
+ array(new MigrateDefaultFieldHandler, 'fields'), $type, $instance,
+ $migration);
+ migrate_instrument_stop('MigrateDefaultFieldHandler->fields');
+ foreach ($subfields as $subfield_name => $subfield_label) {
+ $fields[$machine_name . ':' . $subfield_name] = $subfield_label;
}
}
}
@@ -128,6 +136,95 @@ abstract class MigrateFieldHandler extends MigrateHandler {
}
}
+/**
+ * A fallback field handler to do basic copying of field data.
+ */
+class MigrateDefaultFieldHandler extends MigrateFieldHandler {
+ public function __construct() {}
+
+ /**
+ * Implements MigrateFieldHandler::fields().
+ *
+ * @param $field_type
+ * @param $field_instance
+ *
+ * @return array
+ */
+ public function fields($field_type, $field_instance) {
+ $field_info = field_info_field($field_instance['field_name']);
+ $fields = array();
+ $first = TRUE;
+ foreach ($field_info['columns'] as $column_name => $column_info) {
+ // The first column is the primary value, which is mapped directly to
+ // the field name - so, don't include it here among the subfields.
+ if ($first) {
+ $first = FALSE;
+ }
+ else {
+ $fields[$column_name] = empty($column_info['description']) ?
+ $column_name : $column_info['description'];
+ }
+ }
+ return $fields;
+ }
+
+ /**
+ * Implements MigrateFieldHandler::prepare().
+ *
+ * @param $entity
+ * @param array $field_info
+ * @param array $instance
+ * @param array $values
+ *
+ * @return null
+ */
+ public function prepare($entity, array $field_info, array $instance,
+ array $values) {
+ $arguments = array();
+ if (isset($values['arguments'])) {
+ $arguments = array_filter($values['arguments']);
+ unset($values['arguments']);
+ }
+ $language = $this->getFieldLanguage($entity, $field_info, $arguments);
+
+ // Get the name of the primary (first) column, which is mapped separately.
+ reset($field_info['columns']);
+ $primary_column = key($field_info['columns']);
+
+ // Setup the standard Field API array for saving.
+ $delta = 0;
+ foreach ($values as $value) {
+ // Handle multivalue arguments (especially for subfields).
+ $delta_arguments = array();
+ foreach ($arguments as $name => $argument) {
+ if (is_array($argument) && array_key_exists($delta, $argument)) {
+ $delta_arguments[$name] = $argument[$delta];
+ }
+ else {
+ $delta_arguments[$name] = $argument;
+ }
+ }
+ $return[$language][$delta] = array($primary_column => $value) +
+ array_intersect_key($delta_arguments, $field_info['columns']);
+ $delta++;
+ }
+
+ return isset($return) ? $return : NULL;
+ }
+
+ /**
+ * Overrides MigrateHandler::handlesType().
+ *
+ * @param string $type
+ *
+ * @return bool
+ */
+ public function handlesType($type) {
+ // We claim to handle any type.
+ return TRUE;
+ }
+}
+
/**
* Base class for creating field handlers for fields with a single value.
*
@@ -238,15 +335,19 @@ class MigrateTextFieldHandler extends MigrateFieldHandler {
public function fields($type, $instance, $migration = NULL) {
$fields = array();
if ($type == 'text_with_summary') {
- $fields['summary'] = t('Subfield: Summary of field contents');
+ $fields['summary'] = t('Subfield: Summary of field contents',
+ array('@doc' => 'http://drupal.org/node/1224042#summary'));
}
if ($instance['settings']['text_processing']) {
- $fields['format'] = t('Subfield: Text format for the field');
+ $fields['format'] = t('Subfield: Text format for the field',
+ array('@doc' => 'http://drupal.org/node/1224042#format'));
}
- $fields['language'] = t('Subfield: Language for the field');
+ $fields['language'] = t('Subfield: Language for the field',
+ array('@doc' => 'http://drupal.org/node/1224042#language'));
return $fields;
}
+
public function prepare($entity, array $field_info, array $instance, array $values) {
if (isset($values['arguments'])) {
$arguments = $values['arguments'];
@@ -351,9 +452,12 @@ class MigrateTaxonomyTermReferenceFieldHandler extends MigrateFieldHandler {
*/
public function fields($type, $instance, $migration = NULL) {
return array(
- 'source_type' => t('Option: Set to \'tid\' when the value is a source ID'),
- 'create_term' => t('Option: Set to TRUE to create referenced terms when necessary'),
- 'ignore_case' => t('Option: Set to TRUE to ignore case differences between source data and existing term names'),
+ 'source_type' => t('Option: Set to \'tid\' when the value is a source ID',
+ array('@doc' => 'http://drupal.org/node/1224042#source_type')),
+ 'create_term' => t('Option: Set to TRUE to create referenced terms when necessary',
+ array('@doc' => 'http://drupal.org/node/1224042#create_term')),
+ 'ignore_case' => t('Option: Set to TRUE to ignore case differences between source data and existing term names',
+ array('@doc' => 'http://drupal.org/node/1224042#ignore_case')),
);
}
@@ -375,6 +479,7 @@ class MigrateTaxonomyTermReferenceFieldHandler extends MigrateFieldHandler {
$tids = $values;
}
elseif ($values) {
+ $vocab_name = $field_info['settings']['allowed_values'][0]['vocabulary'];
$names = taxonomy_vocabulary_get_names();
// Get the vocabulary for this term
@@ -382,10 +487,12 @@ class MigrateTaxonomyTermReferenceFieldHandler extends MigrateFieldHandler {
$vid = $field_info['settings']['allowed_values'][0]['vid'];
}
else {
- $vocab_name = $field_info['settings']['allowed_values'][0]['vocabulary'];
$vid = $names[$vocab_name]->vid;
}
+ // Remove leading and trailing spaces in term names
+ $values = array_map('trim', $values);
+
// Cannot use taxonomy_term_load_multiple() since we have an array of names.
// It wants a singular value. This query may return case-insensitive
// matches.
@@ -398,17 +505,33 @@ class MigrateTaxonomyTermReferenceFieldHandler extends MigrateFieldHandler {
// If we're ignoring case, change both the matched term name keys and the
// source values to lowercase.
if (isset($arguments['ignore_case']) && $arguments['ignore_case']) {
+ $ignore_case = TRUE;
$existing_terms = array_change_key_case($existing_terms);
- $values = array_map('strtolower', $values);
+ foreach ($values as $value) {
+ $lower_values[$value] = strtolower($value);
+ }
+ }
+ else {
+ $ignore_case = FALSE;
}
foreach ($values as $value) {
if (isset($existing_terms[$value])) {
$tids[] = $existing_terms[$value];
}
+ elseif ($ignore_case && isset($existing_terms[$lower_values[$value]])) {
+ $tids[] = $existing_terms[$lower_values[$value]];
+ }
elseif (!empty($arguments['create_term'])) {
$new_term = new stdClass();
$new_term->vid = $vid;
$new_term->name = $value;
+ $new_term->vocabulary_machine_name = $vocab_name;
+
+ // This term is being created with no fields, but we should still call
+ // field_attach_validate() before saving, as that invokes
+ // hook_field_attach_validate().
+ field_attach_validate('taxonomy_term', $new_term);
+
taxonomy_term_save($new_term);
$tids[] = $new_term->tid;
// Add newly created term to existing array.
@@ -478,7 +601,7 @@ abstract class MigrateFileFieldBaseHandler extends MigrateFieldHandler {
$file_class = $mapping->getDefaultValue();
}
}
- if (!isset($file_class)) {
+ if (empty($file_class)) {
$file_class = 'MigrateFileUri';
}
$fields += call_user_func(array($file_class, 'fields'));
@@ -502,7 +625,7 @@ abstract class MigrateFileFieldBaseHandler extends MigrateFieldHandler {
$arguments = array();
}
- $language = $this->getFieldLanguage($entity, $field_info, $arguments);
+ $default_language = $this->getFieldLanguage($entity, $field_info, $arguments);
$migration = Migration::currentMigration();
// One can override the source class via CLI or drushrc.php (the
@@ -561,6 +684,10 @@ abstract class MigrateFileFieldBaseHandler extends MigrateFieldHandler {
// MigrateFile class has saved a message indicating why.
if ($file) {
$field_array = array('fid' => $file->fid);
+ $language = isset($instance_arguments['language']) ? $instance_arguments['language'] : $default_language;
+ if (is_array($language)) {
+ $language = $language[$delta];
+ }
$return[$language][] = $this->buildFieldArray($field_array, $instance_arguments, $delta);
}
}
@@ -629,8 +756,10 @@ class MigrateFileFieldHandler extends MigrateFileFieldBaseHandler {
public function fields($type, $instance, $migration = NULL) {
$fields = parent::fields($type, $instance, $migration);
$fields += array(
- 'description' => t('Subfield: String to be used as the description value'),
- 'display' => t('Subfield: String to be used as the display value'),
+ 'description' => t('Subfield: String to be used as the description value',
+ array('@doc' => 'http://drupal.org/node/1224042#description')),
+ 'display' => t('Subfield: String to be used as the display value',
+ array('@doc' => 'http://drupal.org/node/1224042#display')),
);
return $fields;
}
@@ -690,8 +819,10 @@ class MigrateImageFieldHandler extends MigrateFileFieldBaseHandler {
public function fields($type, $instance, $migration = NULL) {
$fields = parent::fields($type, $instance, $migration);
$fields += array(
- 'alt' => t('Subfield: String to be used as the alt value'),
- 'title' => t('Subfield: String to be used as the title value'),
+ 'alt' => t('Subfield: String to be used as the alt value',
+ array('@doc' => 'http://drupal.org/node/1224042#alt')),
+ 'title' => t('Subfield: String to be used as the title value',
+ array('@doc' => 'http://drupal.org/node/1224042#title')),
);
return $fields;
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/file.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/file.inc
index 98960730..e351df2b 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/file.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/file.inc
@@ -35,13 +35,110 @@ interface MigrateFileInterface {
}
abstract class MigrateFileBase implements MigrateFileInterface {
+ /**
+ * Extension of the core FILE_EXISTS_* constants, offering an alternative to
+ * reuse the existing file if present as-is (core only offers the options of
+ * replacing it or renaming to avoid collision).
+ */
+ const FILE_EXISTS_REUSE = -1;
+
+ /**
+ * An optional file object to use as a default starting point for building the
+ * file entity.
+ *
+ * @var stdClass
+ */
+ protected $defaultFile;
+
+ /**
+ * How to handle destination filename collisions.
+ *
+ * @var int
+ */
+ protected $fileReplace = FILE_EXISTS_RENAME;
+
+ /**
+ * Set to TRUE to prevent file deletion on rollback.
+ *
+ * @var bool
+ */
+ protected $preserveFiles = FALSE;
+
+ public function __construct($arguments = array(), $default_file = NULL) {
+ if (isset($arguments['preserve_files'])) {
+ $this->preserveFiles = $arguments['preserve_files'];
+ }
+ if (isset($arguments['file_replace'])) {
+ $this->fileReplace = $arguments['file_replace'];
+ }
+ if ($default_file) {
+ $this->defaultFile = $default_file;
+ }
+ else {
+ $this->defaultFile = new stdClass;
+ }
+ }
+
/**
* Default implementation of MigrateFileInterface::fields().
*
* @return array
*/
static public function fields() {
- return array();
+ return array(
+ 'preserve_files' => t('Option: Boolean indicating whether files should be preserved or deleted on rollback',
+ array('@doc' => 'http://drupal.org/node/1540106#preserve_files')),
+
+ );
+ }
+
+ /**
+ * Setup a file entity object suitable for saving.
+ *
+ * @param $destination
+ * Path to the Drupal copy of the file.
+ * @param $owner
+ * Uid of the file owner.
+ * @return stdClass
+ * A file object ready to be saved.
+ */
+ protected function createFileEntity($destination, $owner) {
+ $file = clone $this->defaultFile;
+ $file->uri = $destination;
+ $file->uid = $owner;
+ if (!isset($file->filename)) {
+ $file->filename = drupal_basename($destination);
+ }
+ if (!isset($file->filemime)) {
+ $file->filemime = file_get_mimetype(urldecode($destination));
+ }
+ if (!isset($file->status)) {
+ $file->status = FILE_STATUS_PERMANENT;
+ }
+
+ if (empty($file->type) || $file->type == 'file') {
+ // Try to determine the file type.
+ if (module_exists('file_entity')) {
+ $type = file_get_type($file);
+ }
+ elseif ($slash_pos = strpos($file->filemime, '/')) {
+ $type = substr($file->filemime, 0, $slash_pos);
+ }
+ $file->type = isset($type) ? $type : 'file';
+ }
+
+ // If we are replacing or reusing an existing filesystem entry,
+ // also re-use its database record.
+ if ($this->fileReplace == FILE_EXISTS_REPLACE ||
+ $this->fileReplace == self::FILE_EXISTS_REUSE) {
+ $existing_files = file_load_multiple(array(), array('uri' => $destination));
+ if (count($existing_files)) {
+ $existing = reset($existing_files);
+ $file->fid = $existing->fid;
+ $file->filename = $existing->filename;
+ }
+ }
+ return $file;
}
/**
@@ -68,6 +165,18 @@ abstract class MigrateFileBase implements MigrateFileInterface {
}
}
+/**
+ * The simplest possible file class - where the value is a remote URI which
+ * simply needs to be saved as the URI on the destination side, with no attempt
+ * to copy or otherwise use it.
+ */
+class MigrateFileUriAsIs extends MigrateFileBase {
+ public function processFile($value, $owner) {
+ $file = file_save($this->createFileEntity($value, $owner));
+ return $file;
+ }
+}
+
/**
* Handle the degenerate case where we already have a file ID.
*/
@@ -92,13 +201,6 @@ class MigrateFileFid extends MigrateFileBase {
* Base class for creating core file entities.
*/
abstract class MigrateFile extends MigrateFileBase {
- /**
- * Extension of the core FILE_EXISTS_* constants, offering an alternative to
- * reuse the existing file if present as-is (core only offers the options of
- * replacing it or renaming to avoid collision).
- */
- const FILE_EXISTS_REUSE = -1;
-
/**
* The destination directory within Drupal.
*
@@ -113,47 +215,14 @@ abstract class MigrateFile extends MigrateFileBase {
*/
protected $destinationFile = '';
- /**
- * How to handle destination filename collisions.
- *
- * @var int
- */
- protected $fileReplace = FILE_EXISTS_RENAME;
-
- /**
- * Set to TRUE to prevent file deletion on rollback.
- *
- * @var bool
- */
- protected $preserveFiles = FALSE;
-
- /**
- * An optional file object to use as a default starting point for building the
- * file entity.
- *
- * @var stdClass
- */
- protected $defaultFile;
-
public function __construct($arguments = array(), $default_file = NULL) {
+ parent::__construct($arguments, $default_file);
if (isset($arguments['destination_dir'])) {
$this->destinationDir = $arguments['destination_dir'];
}
if (isset($arguments['destination_file'])) {
$this->destinationFile = $arguments['destination_file'];
}
- if (isset($arguments['file_replace'])) {
- $this->fileReplace = $arguments['file_replace'];
- }
- if (isset($arguments['preserve_files'])) {
- $this->preserveFiles = $arguments['preserve_files'];
- }
- if ($default_file) {
- $this->defaultFile = $default_file;
- }
- else {
- $this->defaultFile = new stdClass;
- }
}
/**
@@ -162,55 +231,16 @@ abstract class MigrateFile extends MigrateFileBase {
* @return array
*/
static public function fields() {
- return array(
+ return parent::fields() + array(
'destination_dir' => t('Subfield: Path within Drupal files directory to store file',
array('@doc' => 'http://drupal.org/node/1540106#destination_dir')),
'destination_file' => t('Subfield: Path within destination_dir to store the file.',
array('@doc' => 'http://drupal.org/node/1540106#destination_file')),
- 'file_replace' => t('Option: Value of $replace in that file function. Does not apply to file_fast(). Defaults to FILE_EXISTS_RENAME.',
+ 'file_replace' => t('Option: Value of $replace in that file function. Defaults to FILE_EXISTS_RENAME.',
array('@doc' => 'http://drupal.org/node/1540106#file_replace')),
- 'preserve_files' => t('Option: Boolean indicating whether files should be preserved or deleted on rollback',
- array('@doc' => 'http://drupal.org/node/1540106#preserve_files')),
);
}
- /**
- * Setup a file entity object suitable for saving.
- *
- * @param $destination
- * Path to the Drupal copy of the file.
- * @param $owner
- * Uid of the file owner.
- * @return stdClass
- * A file object ready to be saved.
- */
- protected function createFileEntity($destination, $owner) {
- $file = clone $this->defaultFile;
- $file->uri = $destination;
- $file->uid = $owner;
- if (!isset($file->filename)) {
- $file->filename = drupal_basename($destination);
- }
- if (!isset($file->filemime)) {
- $file->filemime = file_get_mimetype($destination);
- }
- if (!isset($file->status)) {
- $file->status = FILE_STATUS_PERMANENT;
- }
- // If we are replacing or reusing an existing filesystem entry,
- // also re-use its database record.
- if ($this->fileReplace == FILE_EXISTS_REPLACE ||
- $this->fileReplace == self::FILE_EXISTS_REUSE) {
- $existing_files = file_load_multiple(array(), array('uri' => $destination));
- if (count($existing_files)) {
- $existing = reset($existing_files);
- $file->fid = $existing->fid;
- $file->filename = $existing->filename;
- }
- }
- return $file;
- }
-
/**
* By whatever appropriate means, put the file in the right place.
*
@@ -242,13 +272,10 @@ abstract class MigrateFile extends MigrateFileBase {
// Our own file_replace behavior - if the file exists, use it without
// replacing it
if ($this->fileReplace == self::FILE_EXISTS_REUSE) {
- // See if we this file already (we'll reuse a file entity if it exists).
+ // See if we this file already (we'll reuse and resave a file entity if it exists).
if (file_exists($destination)) {
$file = $this->createFileEntity($destination, $owner);
- // File entity didn't already exist, create it
- if (empty($file->fid)) {
- $file = file_save($file);
- }
+ $file = file_save($file);
$this->markForPreservation($file->fid);
return $file;
}
@@ -257,7 +284,8 @@ abstract class MigrateFile extends MigrateFileBase {
}
// Prepare the destination directory.
- if (!file_prepare_directory(drupal_dirname($destination),
+ $destdir = drupal_dirname($destination);
+ if (!file_prepare_directory($destdir,
FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
$migration->saveMessage(t('Could not create destination directory for !dest',
array('!dest' => $destination)));
@@ -267,8 +295,7 @@ abstract class MigrateFile extends MigrateFileBase {
// Determine whether we can perform this operation based on overwrite rules.
$destination = file_destination($destination, $this->fileReplace);
if ($destination === FALSE) {
- $migration->saveMessage(t('The file could not be copied because ' .
- 'file %dest already exists in the destination directory.',
+ $migration->saveMessage(t('The file could not be copied because file %dest already exists in the destination directory.',
array('%dest' => $destination)));
return FALSE;
}
@@ -318,11 +345,19 @@ class MigrateFileUri extends MigrateFile {
*/
protected $sourcePath = '';
+ /**
+ * Whether to apply rawurlencode to the components of an incoming file path.
+ */
+ protected $urlEncode = TRUE;
+
public function __construct($arguments = array(), $default_file = NULL) {
parent::__construct($arguments, $default_file);
if (isset($arguments['source_dir'])) {
$this->sourceDir = rtrim($arguments['source_dir'], "/\\");
}
+ if (isset($arguments['urlencode'])) {
+ $this->urlEncode = $arguments['urlencode'];
+ }
}
/**
@@ -335,6 +370,8 @@ class MigrateFileUri extends MigrateFile {
array(
'source_dir' => t('Subfield: Path to source file.',
array('@doc' => 'http://drupal.org/node/1540106#source_dir')),
+ 'urlencode' => t('Option: Encode all segments of the incoming path (defaults to TRUE).',
+ array('@doc' => 'http://drupal.org/node/1540106#urlencode')),
);
}
@@ -348,12 +385,13 @@ class MigrateFileUri extends MigrateFile {
* TRUE if the copy succeeded, FALSE otherwise.
*/
protected function copyFile($destination) {
- // Perform the copy operation, with a cleaned-up path.
- $this->sourcePath = self::urlencode($this->sourcePath);
+ if ($this->urlEncode) {
+ // Perform the copy operation, with a cleaned-up path.
+ $this->sourcePath = self::urlencode($this->sourcePath);
+ }
if (!@copy($this->sourcePath, $destination)) {
$migration = Migration::currentMigration();
- $migration->saveMessage(t('The specified file %file could not be copied to ' .
- '%destination.',
+ $migration->saveMessage(t('The specified file %file could not be copied to %destination.',
array('%file' => $this->sourcePath, '%destination' => $destination)));
return FALSE;
}
@@ -377,8 +415,10 @@ class MigrateFileUri extends MigrateFile {
$components[$key] = rawurlencode($component);
}
$filename = implode('/', $components);
- // Actually, we don't want colons encoded
+ // Actually, we don't want certain characters encoded
$filename = str_replace('%3A', ':', $filename);
+ $filename = str_replace('%3F', '?', $filename);
+ $filename = str_replace('%26', '&', $filename);
}
return $filename;
}
@@ -473,6 +513,9 @@ class MigrateDestinationFile extends MigrateDestinationEntity {
* @var string
*/
protected $fileClass;
+ public function setFileClass($file_class) {
+ $this->fileClass = $file_class;
+ }
/**
* Boolean indicating whether we should avoid deleting the actual file on
@@ -521,10 +564,10 @@ class MigrateDestinationFile extends MigrateDestinationEntity {
public function fields($migration = NULL) {
$fields = array();
// First the core properties
- $fields['fid'] = t('File: Existing file ID');
- $fields['uid'] = t('File: Uid of user associated with file');
- $fields['value'] = t('File: Representation of the source file (usually a URI)');
- $fields['timestamp'] = t('File: UNIX timestamp for the date the file was added');
+ $fields['fid'] = t('Existing file ID');
+ $fields['uid'] = t('Uid of user associated with file');
+ $fields['value'] = t('Representation of the source file (usually a URI)');
+ $fields['timestamp'] = t('UNIX timestamp for the date the file was added');
// Then add in anything provided by handlers
$fields += migrate_handler_invoke_all('Entity', 'fields', $this->entityType, $this->bundle, $migration);
@@ -559,12 +602,14 @@ class MigrateDestinationFile extends MigrateDestinationEntity {
else {
$preserve_files = FALSE;
}
+ $this->prepareRollback($fid);
if ($preserve_files) {
$this->fileDelete($file);
}
else {
file_delete($file, TRUE);
}
+ $this->completeRollback($fid);
migrate_instrument_stop('file_delete');
}
}
@@ -616,6 +661,19 @@ class MigrateDestinationFile extends MigrateDestinationEntity {
$old_file = file_load($file->fid);
}
+ // 'type' is the bundle property on file entities. It must be set here for
+ // the sake of the prepare handlers, although it may be overridden later
+ // based on the detected mime type.
+ if (empty($file->type)) {
+ // If a bundle was specified in the constructor we use it for filetype.
+ if ($this->bundle != 'file') {
+ $file->type = $this->bundle;
+ }
+ else {
+ $file->type = 'file';
+ }
+ }
+
// Invoke migration prepare handlers
$this->prepare($file, $row);
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/node.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/node.inc
index 57414752..59a9513e 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/node.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/node.inc
@@ -12,6 +12,8 @@
* Destination class implementing migration into nodes.
*/
class MigrateDestinationNode extends MigrateDestinationEntity {
+ protected $bypassDestIdCheck = FALSE;
+
static public function getKeySchema() {
return array(
'nid' => array(
@@ -66,29 +68,29 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
array('@doc' => 'http://drupal.org/node/1349696#title'))
. $node_type->title_label . '';
}
- $fields['uid'] = t('Node: Authored by (uid)',
+ $fields['uid'] = t('Authored by (uid)',
array('@doc' => 'http://drupal.org/node/1349696#uid'));
- $fields['created'] = t('Node: Created timestamp',
+ $fields['created'] = t('Created timestamp',
array('@doc' => 'http://drupal.org/node/1349696#created'));
- $fields['changed'] = t('Node: Modified timestamp',
+ $fields['changed'] = t('Modified timestamp',
array('@doc' => 'http://drupal.org/node/1349696#changed'));
- $fields['status'] = t('Node: Published',
+ $fields['status'] = t('Published',
array('@doc' => 'http://drupal.org/node/1349696#status'));
- $fields['promote'] = t('Node: Promoted to front page',
+ $fields['promote'] = t('Promoted to front page',
array('@doc' => 'http://drupal.org/node/1349696#promote'));
- $fields['sticky'] = t('Node: Sticky at top of lists',
+ $fields['sticky'] = t('Sticky at top of lists',
array('@doc' => 'http://drupal.org/node/1349696#sticky'));
- $fields['revision'] = t('Node: Create new revision',
+ $fields['revision'] = t('Create new revision',
array('@doc' => 'http://drupal.org/node/1349696#revision'));
- $fields['log'] = t('Node: Revision Log message',
+ $fields['log'] = t('Revision Log message',
array('@doc' => 'http://drupal.org/node/1349696#log'));
- $fields['language'] = t('Node: Language (fr, en, ...)',
+ $fields['language'] = t('Language (fr, en, ...)',
array('@doc' => 'http://drupal.org/node/1349696#language'));
- $fields['tnid'] = t('Node: The translation set id for this node',
+ $fields['tnid'] = t('The translation set id for this node',
array('@doc' => 'http://drupal.org/node/1349696#tnid'));
- $fields['translate'] = t('Node: A boolean indicating whether this translation page needs to be updated',
+ $fields['translate'] = t('A boolean indicating whether this translation page needs to be updated',
array('@doc' => 'http://drupal.org/node/1349696#translate'));
- $fields['revision_uid'] = t('Node: Modified (uid)',
+ $fields['revision_uid'] = t('Modified (uid)',
array('@doc' => 'http://drupal.org/node/1349696#revision_uid'));
$fields['is_new'] = t('Option: Indicates a new node with the specified nid should be created',
array('@doc' => 'http://drupal.org/node/1349696#is_new'));
@@ -128,7 +130,7 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
public function import(stdClass $node, stdClass $row) {
// Updating previously-migrated content?
$migration = Migration::currentMigration();
- if (isset($row->migrate_map_destid1)) {
+ if (isset($row->migrate_map_destid1) && !$this->bypassDestIdCheck) {
// Make sure is_new is off
$node->is_new = FALSE;
if (isset($node->nid)) {
@@ -177,7 +179,8 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
$node->uid = $old_node->uid;
}
}
- elseif (!isset($node->type)) {
+
+ if (!isset($node->type)) {
// Default the type to our designated destination bundle (by doing this
// conditionally, we permit some flexibility in terms of implementing
// migrations which can affect more than one type).
@@ -207,7 +210,12 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
if (isset($node->uid)) {
$uid = $node->uid;
}
+ if (isset($node->revision)) {
+ $revision = $node->revision;
+ }
+
node_object_prepare($node);
+
if (isset($created)) {
$node->created = $created;
}
@@ -215,6 +223,9 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
if (isset($uid)) {
$node->uid = $uid;
}
+ if (isset($revision)) {
+ $node->revision = $revision;
+ }
}
// Invoke migration prepare handlers
@@ -246,6 +257,14 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
$updating = FALSE;
}
+ // Make sure that if is_new is not TRUE, it is not present.
+ if (isset($node->is_new) && empty($node->is_new)) {
+ unset($node->is_new);
+ }
+
+ // Validate field data prior to saving.
+ field_attach_validate('node', $node);
+
migrate_instrument_start('node_save');
node_save($node);
migrate_instrument_stop('node_save');
@@ -296,3 +315,136 @@ class MigrateDestinationNode extends MigrateDestinationEntity {
return $return;
}
}
+
+/**
+ * Allows you to import revisions.
+ *
+ * Adapted from http://www.darrenmothersele.com/blog/2012/07/16/migrating-node-revisions-drupal-7/
+ *
+ * Class MigrateDestinationNodeRevision
+ *
+ * @author darrenmothersele
+ * @author cthos
+ */
+class MigrateDestinationNodeRevision extends MigrateDestinationNode {
+ /**
+ * Basic initialization.
+ *
+ * @see parent::__construct
+ *
+ * @param string $bundle
+ * A.k.a. the content type (page, article, etc.) of the node.
+ * @param array $options
+ * Options applied to nodes.
+ */
+ public function __construct($bundle, array $options = array()) {
+ parent::__construct($bundle, $options);
+
+ $this->bypassDestIdCheck = TRUE;
+ }
+
+ /**
+ * Get key schema for the node revision destination.
+ *
+ * @see MigrateDestination::getKeySchema
+ *
+ * @return array
+ * Returns the key schema.
+ */
+ static public function getKeySchema() {
+ return array(
+ 'vid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'description' => 'ID of destination node revision',
+ ),
+ );
+ }
+
+ /**
+ * Returns additional fields on top of node destinations.
+ *
+ * @param string $migration
+ * Active migration
+ *
+ * @return array
+ * Fields.
+ */
+ public function fields($migration = NULL) {
+ $fields = parent::fields($migration);
+ $fields['vid'] = t('Node: Revision (vid)', array('@doc' => 'http://drupal.org/node/1298724'));
+ return $fields;
+ }
+
+ /**
+ * Rolls back any versions that have been created.
+ *
+ * @param array $vids
+ * Version ids to roll back.
+ */
+ public function bulkRollback(array $vids) {
+ migrate_instrument_start('revision_delete_multiple');
+ $this->prepareRollback($vids);
+ $nids = array();
+ foreach ($vids as $vid) {
+ if ($revision = node_load(NULL, $vid)) {
+ db_delete('node_revision')
+ ->condition('vid', $revision->vid)
+ ->execute();
+ module_invoke_all('node_revision_delete', $revision);
+ field_attach_delete_revision('node', $revision);
+ $nids[$revision->nid] = $revision->nid;
+ }
+ }
+ $this->completeRollback($vids);
+ foreach ($nids as $nid) {
+ $vid = db_select('node_revision', 'nr')->fields('nr', array('vid'))->condition('nid', $nid, '=')->execute()->fetchField();
+ if (!empty($vid)) {
+ db_update('node')->fields(array('vid' => $vid))->condition('nid', $nid, '=')->execute();
+ }
+ }
+ migrate_instrument_stop('revision_delete_multiple');
+ }
+
+ /**
+ * Overridden import method.
+ *
+ * This is done because parent::import will return the nid of the newly
+ * created nodes. This is bad since the migrate_map_* table will have
+ * nids instead of vids, which could cause a nightmare explosion on
+ * rollback.
+ *
+ * @param stdClass $node
+ * Populated entity.
+ *
+ * @param stdClass $row
+ * Source information in object format.
+ *
+ * @return array|bool
+ * Array with newly created vid, or FALSE on error.
+ *
+ * @throws MigrateException
+ */
+ public function import(stdClass $node, stdClass $row) {
+ // We're importing revisions, this should be set.
+ $node->revision = 1;
+
+ if (empty($node->nid)) {
+ throw new MigrateException(t('Missing incoming nid.'));
+ }
+
+ $original_updated = $this->numUpdated;
+
+ parent::import($node, $row);
+
+ // Reset num updated and increment created since new revision is always an update.
+ $this->numUpdated = $original_updated;
+ $this->numCreated++;
+
+ if (empty($node->vid)) {
+ return FALSE;
+ }
+
+ return array($node->vid);
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/path.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/path.inc
index 5dd26f52..bc452ef2 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/path.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/path.inc
@@ -10,25 +10,32 @@ class MigratePathEntityHandler extends MigrateDestinationHandler {
$this->registerTypes(array('entity'));
}
- public function fields($entity_type, $bundle) {
+ /**
+ * Implementation of MigrateDestinationHandler::fields().
+ */
+ public function fields($entity_type, $bundle, $migration = NULL) {
if (module_exists('path')) {
- switch ($entity_type) {
- case 'node':
- return array('path' => t('Node: Path alias'));
- case 'user':
- return array('path' => t('User: Path alias'));
- case 'taxonomy_term':
- return array('path' => t('Term: Path alias'));
- }
+ return array('path' => t('Path alias'));
}
return array();
}
public function prepare($entity, stdClass $row) {
if (module_exists('path') && isset($entity->path)) {
- $path = $entity->path;
- $entity->path = array();
- $entity->path['alias'] = $path;
+ // Make sure the alias doesn't already exist
+ $query = db_select('url_alias')
+ ->condition('alias', $entity->path)
+ ->condition('language', $entity->language);
+ $query->addExpression('1');
+ $query->range(0, 1);
+ if (!$query->execute()->fetchField()) {
+ $path = $entity->path;
+ $entity->path = array();
+ $entity->path['alias'] = $path;
+ }
+ else {
+ unset($entity->path);
+ }
}
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/poll.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/poll.inc
index 4a16c7b4..cd9b6221 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/poll.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/poll.inc
@@ -60,13 +60,16 @@ class MigratePollEntityHandler extends MigrateDestinationHandler {
$this->registerTypes(array('node'));
}
- public function fields($entity_type, $bundle) {
+ /**
+ * Implementation of MigrateDestinationHandler::fields().
+ */
+ public function fields($entity_type, $bundle, $migration = NULL) {
if ($bundle == 'poll') {
$fields = array(
- 'active' => t('Poll: Active status'),
- 'runtime' => t('Poll: How long the poll runs for in seconds'),
- 'choice' => t('Poll: Choices. Each choice is an array with chtext, chvotes, and weight keys.'),
- 'votes' => t('Poll: Votes. Each vote is an array with chid (or chtext), uid, hostname, and timestamp keys'),
+ 'active' => t('Active status'),
+ 'runtime' => t('How long the poll runs for in seconds'),
+ 'choice' => t('Choices. Each choice is an array with chtext, chvotes, and weight keys.'),
+ 'votes' => t('Votes. Each vote is an array with chid (or chtext), uid, hostname, and timestamp keys'),
);
}
else {
@@ -97,8 +100,7 @@ class MigratePollEntityHandler extends MigrateDestinationHandler {
// Insert actual votes.
foreach ($row->votes as $vote) {
- $chid = $vote['chid'];
- if (!isset($chid)) {
+ if (!isset($vote['chid'])) {
$result = db_select('poll_choice', 'pc')
->fields('pc', array('chid'))
->condition('pc.nid', $entity->nid)
@@ -106,6 +108,9 @@ class MigratePollEntityHandler extends MigrateDestinationHandler {
->execute();
$chid = $result->fetchField();
}
+ else {
+ $chid = $vote['chid'];
+ }
db_insert('poll_vote')
->fields(array(
'chid' => $chid,
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/statistics.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/statistics.inc
index 81d6a8bb..fed051c1 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/statistics.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/statistics.inc
@@ -10,12 +10,15 @@ class MigrateStatisticsEntityHandler extends MigrateDestinationHandler {
$this->registerTypes(array('node'));
}
- public function fields() {
+ /**
+ * Implementation of MigrateDestinationHandler::fields().
+ */
+ public function fields($entity_type, $bundle, $migration = NULL) {
if (module_exists('statistics')) {
$fields = array(
- 'totalcount' => t('Node: The total number of times the node has been viewed.'),
- 'daycount' => t('Node: The total number of times the node has been viewed today.'),
- 'timestamp' => t('Node: The most recent time the node has been viewed.'),
+ 'totalcount' => t('The total number of times the node has been viewed.'),
+ 'daycount' => t('The total number of times the node has been viewed today.'),
+ 'timestamp' => t('The most recent time the node has been viewed.'),
);
}
else {
@@ -26,9 +29,12 @@ class MigrateStatisticsEntityHandler extends MigrateDestinationHandler {
public function complete($node, stdClass $row) {
if (module_exists('statistics') && isset($node->nid)) {
- $totalcount = isset($node->totalcount) ? $node->totalcount : 0;
- $daycount = isset($node->daycount) ? $node->daycount : 0;
- $timestamp = isset($node->timestamp) ? $node->timestamp : 0;
+ $totalcount = isset($node->totalcount) && is_numeric($node->totalcount) ?
+ $node->totalcount : 0;
+ $daycount = isset($node->daycount) && is_numeric($node->daycount) ?
+ $node->daycount : 0;
+ $timestamp = isset($node->timestamp) && is_numeric($node->timestamp) ?
+ $node->timestamp : 0;
db_merge('node_counter')
->key(array('nid' => $node->nid))
->fields(array(
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table.inc
index 4696a20d..627e2a88 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table.inc
@@ -57,19 +57,20 @@ class MigrateDestinationTable extends MigrateDestination {
/**
* Delete a single row.
*
- * @param $id
- * Primary key values.
+ * @param array $ids
+ * The primary key values of the row to be deleted.
*/
- public function rollback(array $id) {
+ public function rollback(array $ids) {
migrate_instrument_start('table rollback');
- $delete = db_delete($this->tableName);
$keys = array_keys(self::getKeySchema($this->tableName));
- $i = 0;
- foreach ($id as $value) {
- $key = $keys[$i++];
+ $values = array_combine($keys, $ids);
+ $this->prepareRollback($values);
+ $delete = db_delete($this->tableName);
+ foreach ($values as $key => $value) {
$delete->condition($key, $value);
}
$delete->execute();
+ $this->completeRollback($values);
migrate_instrument_stop('table rollback');
}
@@ -165,7 +166,7 @@ class MigrateDestinationTable extends MigrateDestination {
public function fields($migration = NULL) {
$fields = array();
foreach ($this->schema['fields'] as $column => $schema) {
- $fields[$column] = t('Type: !type', array('!type' => $schema['type']));
+ $fields[$column] = t('!type', array('!type' => $schema['type']));
}
return $fields;
}
@@ -207,4 +208,44 @@ class MigrateDestinationTable extends MigrateDestination {
$migration->complete($entity, $source_row);
}
}
+
+ /**
+ * Give handlers a shot at cleaning up before the row has been rolled back.
+ *
+ * @param array $ids
+ * The primary key values of the row about to be deleted, keyed by field
+ * name.
+ */
+ public function prepareRollback(array $ids) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+
+ // Call any general handlers.
+ migrate_handler_invoke_all('table', 'prepareRollback', $ids);
+
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'prepareRollback')) {
+ $migration->prepareRollback($ids);
+ }
+ }
+
+ /**
+ * Give handlers a shot at cleaning up after a row has been rolled back.
+ *
+ * @param array $ids
+ * The primary key values of the row which has been deleted, keyed by field
+ * name.
+ */
+ public function completeRollback(array $ids) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+
+ // Call any general handlers.
+ migrate_handler_invoke_all('table', 'completeRollback', $ids);
+
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'completeRollback')) {
+ $migration->completeRollback($ids);
+ }
+ }
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table_copy.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table_copy.inc
index e20f5bec..90ba03c3 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table_copy.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/table_copy.inc
@@ -49,13 +49,18 @@ class MigrateDestinationTableCopy extends MigrateDestination {
$migration = MigrationBase::currentMigration();
$fields = clone $row;
+ // Remove all map data, otherwise we'll try to write it to the destination
+ // table.
+ foreach ($fields as $field => $data) {
+ if (strpos($field, 'migrate_map_') === 0) {
+ unset($fields->$field);
+ }
+ }
$keys = array_keys($this->keySchema);
$values = array();
foreach ($keys as $key) {
$values[] = $row->$key;
}
- unset($fields->migrate_map_destid1);
- unset($fields->needs_update);
$query = db_merge($this->tableName)->key($keys, $values)->fields((array)$fields);
try {
$status = $query->execute();
@@ -72,7 +77,7 @@ class MigrateDestinationTableCopy extends MigrateDestination {
Migration::displayMessage($e->getMessage());
}
catch (Exception $e) {
- $this->handleException($e);
+ $migration->handleException($e);
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/term.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/term.inc
index 24b8282c..db4fe24d 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/term.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/term.inc
@@ -70,20 +70,20 @@ class MigrateDestinationTerm extends MigrateDestinationEntity {
public function fields($migration = NULL) {
$fields = array();
// First the core (taxonomy_term_data table) properties
- $fields['tid'] = t('Term: Existing term ID',
+ $fields['tid'] = t('Existing term ID',
array('@doc' => 'http://drupal.org/node/1349702#tid'));
- $fields['name'] = t('Term: Name',
+ $fields['name'] = t('Name',
array('@doc' => 'http://drupal.org/node/1349702#name'));
- $fields['description'] = t('Term: Description',
+ $fields['description'] = t('Description',
array('@doc' => 'http://drupal.org/node/1349702#description'));
- $fields['parent'] = t('Term: Parent (by Drupal term ID)',
+ $fields['parent'] = t('Parent (by Drupal term ID)',
array('@doc' => 'http://drupal.org/node/1349702#parent'));
// TODO: Remove parent_name, implement via arguments
- $fields['parent_name'] = t('Term: Parent (by name)',
+ $fields['parent_name'] = t('Parent (by name)',
array('@doc' => 'http://drupal.org/node/1349702#parent_name'));
- $fields['format'] = t('Term: Format',
+ $fields['format'] = t('Format',
array('@doc' => 'http://drupal.org/node/1349702#format'));
- $fields['weight'] = t('Term: Weight',
+ $fields['weight'] = t('Weight',
array('@doc' => 'http://drupal.org/node/1349702#weight'));
// Then add in anything provided by handlers
@@ -149,6 +149,12 @@ class MigrateDestinationTerm extends MigrateDestinationEntity {
$term->tid = $row->migrate_map_destid1;
}
}
+
+ // Default to bundle if no vocabulary machine name provided
+ if (!isset($term->vocabulary_machine_name)) {
+ $term->vocabulary_machine_name = $this->bundle;
+ }
+
if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
if (!isset($term->tid)) {
throw new MigrateException(t('System-of-record is DESTINATION, but no destination tid provided'));
@@ -166,10 +172,6 @@ class MigrateDestinationTerm extends MigrateDestinationEntity {
$term = $old_term;
}
else {
- // Default to bundle if no vocabulary machine name provided
- if (!isset($term->vocabulary_machine_name)) {
- $term->vocabulary_machine_name = $this->bundle;
- }
// vid is required
if (empty($term->vid)) {
static $vocab_map = array();
@@ -203,7 +205,13 @@ class MigrateDestinationTerm extends MigrateDestinationEntity {
if (empty($term->parent)) {
$term->parent = array(0);
}
- if (is_array($term->parent) && isset($term->parent['arguments'])) {
+ elseif (!is_array($term->parent)) {
+ // Convert to an array for comparison in findMatchingTerm().
+ // Note: taxonomy_term_save() also normalizes to an array.
+ $term->parent = array($term->parent);
+ }
+
+ if (isset($term->parent['arguments'])) {
// Unset arguments here to avoid duplicate entries in the
// term_hierarchy table.
unset($term->parent['arguments']);
@@ -211,8 +219,15 @@ class MigrateDestinationTerm extends MigrateDestinationEntity {
if (!isset($term->format)) {
$term->format = $this->textFormat;
}
+ if (!isset($term->language)) {
+ $term->language = $this->language;
+ }
$this->prepare($term, $row);
+ if (empty($term->name)) {
+ throw new MigrateException(t('Taxonomy term name is required.'));
+ }
+
if (!$this->allowDuplicateTerms && $existing_term = $this->findMatchingTerm($term)) {
foreach ($existing_term as $field => $value) {
if (!isset($term->$field)) {
@@ -242,6 +257,9 @@ class MigrateDestinationTerm extends MigrateDestinationEntity {
$updating = FALSE;
}
+ // Validate field data prior to saving.
+ field_attach_validate('taxonomy_term', $term);
+
migrate_instrument_start('taxonomy_term_save');
$status = taxonomy_term_save($term);
migrate_instrument_stop('taxonomy_term_save');
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/user.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/user.inc
index 431d5a98..b3e9fcd3 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/user.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/user.inc
@@ -75,41 +75,41 @@ class MigrateDestinationUser extends MigrateDestinationEntity {
public function fields($migration = NULL) {
$fields = array();
// First the core (users table) properties
- $fields['uid'] = t('User: Existing user ID',
+ $fields['uid'] = t('Existing user ID',
array('@doc' => 'http://drupal.org/node/1349632#uid'));
- $fields['mail'] = t('User: Email address',
+ $fields['mail'] = t('Email address',
array('@doc' => 'http://drupal.org/node/1349632#mail'));
- $fields['name'] = t('User: Username',
+ $fields['name'] = t('Username',
array('@doc' => 'http://drupal.org/node/1349632#name'));
- $fields['pass'] = t('User: Password (plain text)',
+ $fields['pass'] = t('Password',
array('@doc' => 'http://drupal.org/node/1349632#pass'));
- $fields['status'] = t('User: Status',
+ $fields['status'] = t('Status',
array('@doc' => 'http://drupal.org/node/1349632#status'));
- $fields['created'] = t('User: Registered timestamp',
+ $fields['created'] = t('Registered timestamp',
array('@doc' => 'http://drupal.org/node/1349632#created'));
- $fields['access'] = t('User: Last access timestamp',
+ $fields['access'] = t('Last access timestamp',
array('@doc' => 'http://drupal.org/node/1349632#access'));
- $fields['login'] = t('User: Last login timestamp',
+ $fields['login'] = t('Last login timestamp',
array('@doc' => 'http://drupal.org/node/1349632#login'));
- $fields['roles'] = t('User: Role IDs',
+ $fields['roles'] = t('Role IDs',
array('@doc' => 'http://drupal.org/node/1349632#roles'));
- $fields['role_names'] = t('User: Role Names',
+ $fields['role_names'] = t('Role Names',
array('@doc' => 'http://drupal.org/node/1349632#role_names'));
- $fields['picture'] = t('User: Picture',
+ $fields['picture'] = t('Picture',
array('@doc' => 'http://drupal.org/node/1349632#picture'));
- $fields['signature'] = t('User: Signature',
+ $fields['signature'] = t('Signature',
array('@doc' => 'http://drupal.org/node/1349632#signature'));
- $fields['signature_format'] = t('User: Signature format',
+ $fields['signature_format'] = t('Signature format',
array('@doc' => 'http://drupal.org/node/1349632#signature_format'));
- $fields['timezone'] = t('User: Timezone',
+ $fields['timezone'] = t('Timezone',
array('@doc' => 'http://drupal.org/node/1349632#timezone'));
- $fields['language'] = t('User: Language',
+ $fields['language'] = t('Language',
array('@doc' => 'http://drupal.org/node/1349632#language'));
- $fields['theme'] = t('User: Default theme',
+ $fields['theme'] = t('Default theme',
array('@doc' => 'http://drupal.org/node/1349632#theme'));
- $fields['init'] = t('User: Init',
+ $fields['init'] = t('Init',
array('@doc' => 'http://drupal.org/node/1349632#init'));
- $fields['data'] = t('User: Data',
+ $fields['data'] = t('Data',
array('@doc' => 'http://drupal.org/node/1349632#init'));
$fields['is_new'] = t('Option: Indicates a new user with the specified uid should be created',
array('@doc' => 'http://drupal.org/node/1349632#is_new'));
@@ -230,6 +230,9 @@ class MigrateDestinationUser extends MigrateDestinationEntity {
$account->login = MigrationBase::timestamp($account->login);
}
+ // Validate field data prior to saving.
+ field_attach_validate('user', $account);
+
migrate_instrument_start('user_save');
$newaccount = user_save($old_account, (array)$account);
migrate_instrument_stop('user_save');
@@ -262,6 +265,14 @@ class MigrateDestinationUser extends MigrateDestinationEntity {
}
else {
$this->numCreated++;
+ // user_save() doesn't update file_usage on account creation, we have
+ // to do it ourselves.
+ if (!empty($newaccount->picture)) {
+ $file = file_load($newaccount->picture);
+ if (is_object($file)) {
+ file_usage_add($file, 'user', 'user', $newaccount->uid);
+ }
+ }
}
$this->complete($newaccount, $row);
$return = array($newaccount->uid);
@@ -324,9 +335,9 @@ class MigrateDestinationRole extends MigrateDestinationTable {
throw new MigrateException(t("Incoming id !id and map destination id !destid don't match",
array('!id' => $entity->rid, '!destid' => $row->migrate_map_destid1)));
}
- else {
- $entity->rid = $row->migrate_map_destid1;
- }
+ }
+ else {
+ $entity->rid = $row->migrate_map_destid1;
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/destinations/variable.inc b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/variable.inc
new file mode 100644
index 00000000..4d755f6f
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/destinations/variable.inc
@@ -0,0 +1,187 @@
+ array(
+ 'description' => 'The name of the variable.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ );
+ }
+
+ public function __construct() {
+ parent::__construct();
+ }
+
+ public function __toString() {
+ $output = t('Variable');
+ return $output;
+ }
+
+ /**
+ * Returns a list of fields available to be mapped for variables.
+ *
+ * @param Migration $migration
+ * Optionally, the migration containing this destination.
+ * @return array
+ * Keys: machine names of the fields (to be passed to addFieldMapping)
+ * Values: Human-friendly descriptions of the fields.
+ */
+ public function fields($migration = NULL) {
+ $fields = array(
+ 'name' => t('The name of the variable.'),
+ 'value' => t('The value of the variable.'),
+ );
+ return $fields;
+ }
+
+ /**
+ * Import a single row.
+ *
+ * @param $variable
+ * Variable object to build. Prefilled with any fields mapped in the Migration.
+ * @param $row
+ * Raw source data object - passed through to prepare/complete handlers.
+ * @return array
+ * Array of key fields of the object that was saved if
+ * successful. FALSE on failure.
+ */
+ public function import(stdClass $variable, stdClass $row) {
+ // Invoke migration prepare handlers
+ $this->prepare($variable, $row);
+
+ // Check to see if this is a new variable.
+ $update = FALSE;
+ // We cannot just check against NULL because a variable might actually be
+ // set to NULL. Attempt to use a unique variable default value that nothing
+ // else would use.
+ $default = 'migrate:' . REQUEST_TIME . ':' . drupal_random_key();
+ if (variable_get($variable->name, $default) !== $default) {
+ $update = TRUE;
+ }
+
+ // variable_set() provides no return callback, so we can't really test this
+ // without running a variable_get() check.
+ migrate_instrument_start('variable_set');
+ variable_set($variable->name, $variable->value);
+ migrate_instrument_stop('variable_set');
+
+ // Return the new id or FALSE on failure.
+ if (variable_get($variable->name, $default) === $variable->value) {
+ // Increment the count if the save succeeded.
+ if ($update) {
+ $this->numUpdated++;
+ }
+ else {
+ $this->numCreated++;
+ }
+ // Return the primary key to the mapping table.
+ $return = array($variable->name);
+ }
+ else {
+ $return = FALSE;
+ }
+
+ // Invoke migration complete handlers.
+ $this->complete($variable, $row);
+
+ return $return;
+ }
+
+ /**
+ * Implementation of MigrateDestination::prepare().
+ */
+ public function prepare($variable, stdClass $row) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ $variable->migrate = array(
+ 'machineName' => $migration->getMachineName(),
+ );
+
+ // Call any general handlers.
+ migrate_handler_invoke_all('variable', 'prepare', $variable, $row);
+ // Then call any prepare handler for this specific Migration.
+ if (method_exists($migration, 'prepare')) {
+ $migration->prepare($variable, $row);
+ }
+ }
+
+ /**
+ * Implementation of MigrateDestination::complete().
+ */
+ public function complete($variable, stdClass $row) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ $variable->migrate = array(
+ 'machineName' => $migration->getMachineName(),
+ );
+ // Call any general handlers.
+ migrate_handler_invoke_all('variable', 'complete', $variable, $row);
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'complete')) {
+ $migration->complete($variable, $row);
+ }
+ }
+
+ /**
+ * Delete a single variable.
+ *
+ * @param $id
+ * Array of fields representing the key (in this case, just variable name).
+ */
+ public function rollback(array $id) {
+ $name = reset($id);
+ migrate_instrument_start('variable_delete');
+ $this->prepareRollback($name);
+ variable_del($name);
+ $this->completeRollback($name);
+ migrate_instrument_stop('variable_delete');
+ }
+
+ /**
+ * Give handlers a shot at cleaning up before a variable has been rolled back.
+ *
+ * @param $name
+ * The name of the variable about to be deleted.
+ */
+ public function prepareRollback($name) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ // Call any general handlers.
+ migrate_handler_invoke_all('variable', 'prepareRollback', $name);
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'prepareRollback')) {
+ $migration->prepareRollback($name);
+ }
+ }
+
+ /**
+ * Give handlers a shot at cleaning up after a variable has been rolled back.
+ *
+ * @param $name
+ * The name of the variable which has been deleted.
+ */
+ public function completeRollback($name) {
+ // We do nothing here but allow child classes to act.
+ $migration = Migration::currentMigration();
+ // Call any general handlers.
+ migrate_handler_invoke_all('variable', 'completeRollback', $name);
+ // Then call any complete handler for this specific Migration.
+ if (method_exists($migration, 'completeRollback')) {
+ $migration->completeRollback($name);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/csv.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/csv.inc
index df3945d4..03d1955b 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/csv.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/csv.inc
@@ -186,6 +186,8 @@ class MigrateSourceCSV extends MigrateSource {
public function getNextRow() {
$row = $this->getNextLine();
if ($row) {
+ // only use rows specified in $this->csvcolumns().
+ $row = array_intersect_key($row, $this->csvcolumns);
// Set meaningful keys for the columns mentioned in $this->csvcolumns().
foreach ($this->csvcolumns as $int => $values) {
list($key, $description) = $values;
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/db2.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/db2.inc
new file mode 100644
index 00000000..22dccb9e
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/db2.inc
@@ -0,0 +1,163 @@
+connection;
+ }
+
+ /**
+ * The SQL query from which to obtain data. Is a string.
+ */
+ protected $query;
+
+ /**
+ * The statement resource from executing the query - traversed to process the
+ * incoming data.
+ */
+ protected $stmt;
+
+ /**
+ * Return an options array for DB2 sources.
+ *
+ * @param boolean $cache_counts
+ * Indicates whether to cache counts of source records.
+ */
+ static public function options($cache_counts = FALSE) {
+ return compact('cache_counts');
+ }
+
+ /**
+ * Simple initialization.
+ */
+ public function __construct(array $configuration, $query, $count_query,
+ array $fields, array $options = array()) {
+ parent::__construct($options);
+ $this->query = $query;
+ $this->countQuery = $count_query;
+ $this->configuration = $configuration;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Return a string representing the source query.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->query;
+ }
+
+ /**
+ * Connect lazily to the DB server.
+ */
+ protected function connect() {
+ if (!isset($this->connection)) {
+ // Check for the ibm_db2 extension before attempting to connect with it.
+ if (!extension_loaded('ibm_db2')) {
+ throw new Exception(t('You must configure the ibm_db2 extension in PHP.'));
+ }
+ // Connect to db2.
+ $this->connection = db2_connect($this->configuration['database'],
+ $this->configuration['username'], $this->configuration['password']);
+ }
+ if ($this->connection) {
+ return TRUE;
+ }
+ // If we failed to connect, throw an exception with the connection error
+ // message.
+ else {
+ $e = db2_conn_errormsg();
+ throw new Exception($e);
+ return FALSE;
+ }
+ }
+
+ /**
+ * Returns a list of fields available to be mapped from the source query.
+ *
+ * @return array
+ * Keys: machine names of the fields (to be passed to addFieldMapping)
+ * Values: Human-friendly descriptions of the fields.
+ */
+ public function fields() {
+ // The fields are passed to the constructor for this plugin.
+ return $this->fields;
+ }
+
+ /**
+ * Return a count of all available source records.
+ */
+ public function computeCount() {
+ migrate_instrument_start('MigrateSourceDB2 count');
+ // Make sure we're connected.
+ if ($this->connect()) {
+ // Execute the count query.
+ $stmt = db2_exec($this->connection, $this->countQuery);
+ // If something went wrong, throw an exception with the error message.
+ if (!$stmt) {
+ $e = db2_stmt_errormsg($stmt);
+ throw new Exception($e);
+ }
+ // Grab the first row as an array.
+ $count_array = db2_fetch_array($stmt);
+ // The first item in this array will be our count.
+ $count = reset($count_array);
+ }
+ else {
+ // Connection failed.
+ $count = FALSE;
+ }
+ migrate_instrument_stop('MigrateSourceDB2 count');
+ return $count;
+ }
+
+ /**
+ * Implementation of MigrateSource::performRewind().
+ */
+ public function performRewind() {
+ migrate_instrument_start('db2_query');
+ // Ensure we're connected to the database.
+ $this->connect();
+ // Execute the query.
+ $this->stmt = db2_exec($this->connection, $this->query);
+ // Throw an exception with the error message if something went wrong.
+ if (!$this->stmt) {
+ $e = db2_stmt_errormsg($this->stmt);
+ throw new Exception($e);
+ }
+ migrate_instrument_stop('db2_query');
+ }
+
+ /**
+ * Implementation of MigrateSource::getNextRow().
+ *
+ * @return object
+ */
+ public function getNextRow() {
+ return db2_fetch_object($this->stmt);
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/files.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/files.inc
index e025d11d..182b3cc8 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/files.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/files.inc
@@ -62,6 +62,7 @@ class MigrateListFiles extends MigrateList {
protected $fileMask;
protected $directoryOptions;
protected $parser;
+ protected $getContents;
/**
* Constructor.
@@ -86,8 +87,12 @@ class MigrateListFiles extends MigrateList {
* @param $options
* Options that will be passed on to file_scan_directory(). See docs of that
* core Drupal function for more information.
+ * @param MigrateContentParser $parser
+ * Content parser class to use.
+ * @param $get_contents
+ * Whether to load the contents of files.
*/
- public function __construct($list_dirs, $base_dir, $file_mask = NULL, $options = array(), MigrateContentParser $parser = NULL) {
+ public function __construct($list_dirs, $base_dir, $file_mask = NULL, $options = array(), MigrateContentParser $parser = NULL, $get_contents = TRUE) {
if (!$parser) {
$parser = new MigrateSimpleContentParser();
}
@@ -97,6 +102,7 @@ class MigrateListFiles extends MigrateList {
$this->fileMask = $file_mask;
$this->directoryOptions = $options;
$this->parser = $parser;
+ $this->getContents = $get_contents;
}
/**
@@ -136,11 +142,16 @@ class MigrateListFiles extends MigrateList {
protected function getIDsFromFiles(array $files) {
$ids = array();
foreach ($files as $file) {
- $contents = file_get_contents($file->uri);
- $this->parser->setContent($contents);
- if ($this->parser->getChunkCount() > 1) {
- foreach ($this->parser->getChunkIDs() as $chunk_id) {
- $ids[] = str_replace($this->baseDir, '', (string) $file->uri) . MIGRATE_CHUNK_SEPARATOR . $chunk_id;
+ if ($this->getContents) {
+ $contents = file_get_contents($file->uri);
+ $this->parser->setContent($contents);
+ if ($this->parser->getChunkCount() > 1) {
+ foreach ($this->parser->getChunkIDs() as $chunk_id) {
+ $ids[] = str_replace($this->baseDir, '', (string) $file->uri) . MIGRATE_CHUNK_SEPARATOR . $chunk_id;
+ }
+ }
+ else {
+ $ids[] = str_replace($this->baseDir, '', (string) $file->uri);
}
}
else {
@@ -206,7 +217,7 @@ class MigrateItemFile extends MigrateItem {
public function getItem($id) {
$pieces = explode(MIGRATE_CHUNK_SEPARATOR ,$id);
$item_uri = $this->baseDir . $pieces[0];
- $chunk = $pieces[1];
+ $chunk = !empty($pieces[1]) ? $pieces[1] : '';
// Get the file data at the specified URI
$data = $this->loadFile($item_uri);
@@ -242,4 +253,4 @@ class MigrateItemFile extends MigrateItem {
}
return $data;
}
-}
\ No newline at end of file
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/json.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/json.inc
index 6b9039a5..6d378bb7 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/json.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/json.inc
@@ -92,7 +92,7 @@ class MigrateListJSON extends MigrateList {
if ($json) {
$data = drupal_json_decode($json);
if ($data) {
- $count = count($data);
+ $count = count($this->getIDsFromJSON($data));
}
}
return $count;
@@ -503,7 +503,7 @@ class MigrateSourceJSON extends MigrateSource {
* @return string
*/
public function activeUrl() {
- if ($this->activeUrl) {
+ if (isset($this->activeUrl)) {
return $this->sourceUrls[$this->activeUrl];
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/list.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/list.inc
index 4ff3e1c9..7a62fa28 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/list.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/list.inc
@@ -56,6 +56,17 @@ abstract class MigrateItem {
* @return stdClass
*/
abstract public function getItem($id);
+
+ /**
+ * Implementors may optionally implement a hash function, applied to the
+ * entire source row, if this particular item type makes it difficult to
+ * do on the raw row.
+ *
+ * @param $row
+ *
+ * @return mixed
+ */
+ //abstract public function hash($row);
}
/**
@@ -192,4 +203,18 @@ class MigrateSourceList extends MigrateSource {
}
return $row;
}
+
+ /**
+ * Overrides MigrateSource::hash().
+ */
+ protected function hash($row) {
+ // Let the item class override the default hash function.
+ if (method_exists($this->itemClass, 'hash')) {
+ $hash = $this->itemClass->hash($row);
+ }
+ else {
+ $hash = parent::hash($row);
+ }
+ return $hash;
+ }
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/mongodb.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/mongodb.inc
new file mode 100644
index 00000000..2c305e35
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/mongodb.inc
@@ -0,0 +1,189 @@
+ 1),
+ array $options = array()) {
+ parent::__construct($options);
+
+ $this->collection = $collection;
+ $this->query = $query;
+ $this->sort = $sort;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Returns a list of fields available to be mapped from the source query.
+ *
+ * @return array
+ * Keys: machine names of the fields (to be passed to addFieldMapping)
+ * Values: Human-friendly descriptions of the fields.
+ */
+ public function fields() {
+ // The fields are passed to the constructor for this plugin.
+ return $this->fields;
+ }
+
+ /**
+ * Return a count of all available source records.
+ */
+ public function computeCount() {
+ return $this->cursor->count(TRUE);
+ }
+
+ /**
+ * Implementation of MigrateSource::getNextRow().
+ *
+ * @return object
+ */
+ public function getNextRow() {
+ $row = $this->cursor->getNext();
+
+ if ($row) {
+ return (object) $row;
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Implementation of MigrateSource::performRewind().
+ *
+ * @return void
+ */
+ public function performRewind() {
+ $keys = $this->getSourceKeyNameAndType();
+
+ // If we have an existing idlist we use it.
+ if ($this->idList) {
+ foreach ($this->idList as $key => $id) {
+ // Try make new ObjectID.
+ $this->idList[$key] = $this->getMongoId($id, $keys);
+ }
+
+ $this->query[$keys[0]['name']]['$in'] = $this->idList;
+ }
+
+ migrate_instrument_start('MigrateSourceMongoDB execute');
+ try {
+ $this->cursor = $this->collection
+ ->find($this->query)
+ ->sort($this->sort);
+ $this->cursor->timeout(-1);
+ } catch (MongoCursorException $e) {
+ Migration::displayMessage($e->getMessage(), 'error');
+ }
+ migrate_instrument_stop('MigrateSourceMongoDB execute');
+ }
+
+ /**
+ * Return a string representing the source query.
+ *
+ * @return string
+ */
+ public function __toString() {
+ if (is_null($this->cursor)) {
+ $this->cursor = $this->collection
+ ->find($this->query)
+ ->sort($this->sort);
+ $this->cursor->timeout(-1);
+ }
+
+ $query_info = $this->cursor->info();
+
+ $query = 'query: ' . drupal_json_encode($query_info['query']['$query']);
+ $sort = 'order by: ' . drupal_json_encode($query_info['query']['$orderby']);
+ $fields = 'fields: ' . drupal_json_encode($query_info['fields']);
+
+ return $query . PHP_EOL .
+ $sort . PHP_EOL .
+ $fields . PHP_EOL;
+ }
+
+ /**
+ * Check if given document id is a mongo ObjectId and return mongo ObjectId
+ * or simple value.
+ *
+ * @param mixed $document_id
+ * Document key value.
+ * @param array $keys
+ * List of keys.
+ * @return type
+ */
+ public function getMongoId($document_id, $keys) {
+ if ($keys[0]['name'] != '_id') {
+ switch ($keys[0]['type']) {
+ case 'int':
+ return (int)$document_id;
+ break;
+ default:
+ return $document_id;
+ }
+ }
+
+ // Trying create Mongo ObjectId
+ $mongoid = new MongoId($document_id);
+
+ // If (string) $mongoid == $document_id we return $mongoid object
+ if ((string) $mongoid == $document_id) {
+ return $mongoid;
+ }
+
+ return $document_id;
+ }
+
+ /**
+ * Get source keys array.
+ */
+ public function getSourceKeyNameAndType() {
+ // Get the key name, and type.
+ $keys = array();
+ foreach ($this->activeMap->getSourceKey() as $field_name => $field_schema) {
+ $keys[] = array(
+ 'name' => $field_name,
+ 'type' => $field_schema['type'],
+ );
+ }
+
+ return $keys;
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/mssql.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/mssql.inc
index 56585b6a..f8d2b1db 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/mssql.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/mssql.inc
@@ -125,7 +125,8 @@ class MigrateSourceMSSQL extends MigrateSource {
migrate_instrument_start('MigrateSourceMSSQL count');
if ($this->connect()) {
$result = mssql_query($this->countQuery);
- $count = reset(mssql_fetch_object($result));
+ $result_array = mssql_fetch_array($result);
+ $count = reset($result_array);
}
else {
// Do something else?
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/multiitems.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/multiitems.inc
index b400e563..c74f9c53 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/multiitems.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/multiitems.inc
@@ -47,8 +47,18 @@ abstract class MigrateItems {
* @return stdClass
*/
abstract public function getItem($id);
-}
+ /**
+ * Implementors may optionally implement a hash function, applied to the
+ * entire source row, if this particular item type makes it difficult to
+ * do on the raw row.
+ *
+ * @param $row
+ *
+ * @return mixed
+ */
+ //abstract public function hash($row);
+}
/**
* Implementation of MigrateItems, for providing a list of IDs and for
@@ -182,5 +192,18 @@ class MigrateSourceMultiItems extends MigrateSource {
}
return $row;
}
-}
+ /**
+ * Overrides MigrateSource::hash().
+ */
+ protected function hash($row) {
+ // Let the item class override the default hash function.
+ if (method_exists($this->itemsClass, 'hash')) {
+ $hash = $this->itemsClass->hash($row);
+ }
+ else {
+ $hash = parent::hash($row);
+ }
+ return $hash;
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/spreadsheet.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/spreadsheet.inc
new file mode 100644
index 00000000..87f37cbc
--- /dev/null
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/spreadsheet.inc
@@ -0,0 +1,238 @@
+file = $path;
+ $this->sheetName = $sheet_name;
+ $this->columns = $columns;
+
+ // Load the workbook.
+ if ($this->load()) {
+ // Get the dimensions of the worksheet.
+ $this->rows = $this->worksheet->getHighestDataRow();
+ $this->cols = PHPExcel_Cell::columnIndexFromString($this->worksheet->getHighestDataColumn());
+
+ // Map field names to their column index.
+ for ($col = 0; $col < $this->cols; ++$col) {
+ $this->fields[$col] = trim($this->worksheet->getCellByColumnAndRow($col, 1)->getValue());
+ }
+
+ $this->unload();
+ }
+ }
+
+ /**
+ * Loads the workbook.
+ *
+ * @return bool
+ * Returns true if the workbook was successfully loaded, otherwise false.
+ */
+ public function load() {
+ // Check that the file exists.
+ if (!file_exists($this->file)) {
+ Migration::displayMessage(t('The file !filename does not exist.', array('!filename' => $this->file)));
+ return FALSE;
+ }
+
+ // Check that required modules are enabled.
+ if (!module_exists('libraries')) {
+ Migration::displayMessage(t('The Libraries API module is not enabled.'));
+ return FALSE;
+ }
+ if (!module_exists('phpexcel')) {
+ Migration::displayMessage(t('The PHPExcel module is not enabled.'));
+ return FALSE;
+ }
+
+ $library = libraries_load('PHPExcel');
+ if (empty($library['loaded'])) {
+ Migration::displayMessage(t('The PHPExcel library could not be found.'));
+ return FALSE;
+ }
+
+ // Load the workbook.
+ try {
+ // Identify the type of the input file.
+ $type = PHPExcel_IOFactory::identify($this->file);
+ // Create a new Reader of the file type.
+ $reader = PHPExcel_IOFactory::createReader($type);
+ // Advise the Reader that we only want to load cell data.
+ $reader->setReadDataOnly(TRUE);
+ // Advise the Reader of which worksheet we want to load.
+ $reader->setLoadSheetsOnly($this->sheetName);
+ // Load the source file.
+ $this->workbook = $reader->load($this->file);
+ $this->worksheet = $this->workbook->getSheet();
+ }
+ catch (Exception $e) {
+ Migration::displayMessage(t('Error loading file: %message', array('%message' => $e->getMessage())));
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Unloads the workbook.
+ */
+ public function unload() {
+ $this->workbook->disconnectWorksheets();
+ unset($this->workbook);
+ }
+
+ /**
+ * Returns a string representing the source query.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->file;
+ }
+
+ /**
+ * Returns a list of fields available to be mapped from the source query.
+ *
+ * @return array
+ * Keys: machine names of the fields (to be passed to addFieldMapping).
+ * Values: Human-friendly descriptions of the fields.
+ */
+ public function fields() {
+ $fields = array();
+
+ foreach ($this->fields as $name) {
+ $fields[$name] = $name;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Returns a count of all available source records.
+ */
+ public function computeCount() {
+ // Subtract 1 for the header.
+ return $this->rows - 1;
+ }
+
+ /**
+ * Implements MigrateSource::performRewind().
+ *
+ * @return void
+ */
+ public function performRewind() {
+ // Initialize the workbook if it isn't already.
+ if (!isset($this->workbook)) {
+ $this->load();
+ }
+
+ $this->rowNumber = 1;
+ }
+
+ /**
+ * Implements MigrateSource::getNextRow().
+ *
+ * @return null|object
+ */
+ public function getNextRow() {
+ migrate_instrument_start('MigrateSourceSpreadsheet::next');
+ ++$this->rowNumber;
+
+ if ($this->rowNumber <= $this->rows) {
+ $row_values = array();
+ for ($col = 0; $col < $this->cols; ++$col) {
+ if (in_array($this->fields[$col], $this->columns) || empty($this->columns)) {
+ $row_values[$this->fields[$col]] = trim($this->worksheet->getCellByColumnAndRow($col, $this->rowNumber)->getValue());
+ }
+ }
+
+ return (object) $row_values;
+ }
+ else {
+ // EOF, close the workbook.
+ $this->unload();
+
+ migrate_instrument_stop('MigrateSourceSpreadsheet::next');
+ return NULL;
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/sql.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/sql.inc
index ca907fc1..fbcd05a8 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/sql.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/sql.inc
@@ -14,7 +14,7 @@ class MigrateSourceSQL extends MigrateSource {
*
* @var SelectQueryInterface
*/
- protected $originalQuery, $query, $countQuery;
+ protected $originalQuery, $query, $countQuery, $alteredQuery;
/**
* Return a reference to the base query, in particular so Migration classes
@@ -24,7 +24,7 @@ class MigrateSourceSQL extends MigrateSource {
* @return SelectQueryInterface
*/
public function &query() {
- return $this->originalQuery();
+ return $this->originalQuery;
}
/**
@@ -42,6 +42,21 @@ class MigrateSourceSQL extends MigrateSource {
*/
protected $numProcessed = 0;
+ /**
+ * Current data batch.
+ *
+ * @var int
+ */
+ protected $batch = 0;
+
+ /**
+ * Number of records to fetch from the database during each batch. A value
+ * of zero indicates no batching is to be done.
+ *
+ * @var int
+ */
+ protected $batchSize = 0;
+
/**
* List of available source fields.
*
@@ -118,6 +133,18 @@ class MigrateSourceSQL extends MigrateSource {
$this->countQuery = $count_query;
}
+ if (isset($options['batch_size'])) {
+ $this->batchSize = $options['batch_size'];
+ // Joining to the map table is incompatible with batching, disable it.
+ $options['map_joinable'] = FALSE;
+ }
+
+ // If we're tracking changes, then we need to fetch all rows to see if
+ // they've changed, we can't make that determination through a direct join.
+ if (!empty($options['track_changes'])) {
+ $options['map_joinable'] = FALSE;
+ }
+
if (isset($options['map_joinable'])) {
$this->mapJoinable = $options['map_joinable'];
}
@@ -153,14 +180,15 @@ class MigrateSourceSQL extends MigrateSource {
}
}
-
/**
* Return a string representing the source query.
*
* @return string
*/
public function __toString() {
- return (string) $this->query;
+ $query = clone $this->query;
+ $query = $query->extend('MigrateConnectionQuery');
+ return $query->getString();
}
/**
@@ -243,6 +271,7 @@ class MigrateSourceSQL extends MigrateSource {
public function performRewind() {
$this->result = NULL;
$this->query = clone $this->originalQuery;
+ $this->batch = 0;
// Get the key values, for potential use in joining to the map table, or
// enforcing idlist.
@@ -259,7 +288,41 @@ class MigrateSourceSQL extends MigrateSource {
// 1. If idlist is provided, then only process items in that list (AND key
// IN (idlist)). Only applicable with single-value keys.
if ($this->idList) {
- $this->query->condition($keys[0], $this->idList, 'IN');
+ $simple_ids = array();
+ $compound_ids = array();
+ $key_count = count($keys);
+
+ foreach ($this->idList as $id) {
+ // Look for multi-key separator. If there is only 1 key, ignore.
+ if (strpos($id, $this->multikeySeparator) === FALSE || $key_count == 1) {
+ $simple_ids[] = $id;
+ continue;
+ }
+
+ $compound_ids[] = explode($this->multikeySeparator, $id);
+ }
+
+ // Check for compunded ids. If present add them with subsequent OR statements.
+ if (!empty($compound_ids)) {
+ $condition = db_or();
+ if (!empty($simple_ids)) {
+ $condition->condition($keys[0], $simple_ids, 'IN');
+ }
+
+ foreach ($compound_ids as $values) {
+ $temp_and = db_and();
+ foreach ($values as $pos => $value) {
+ $temp_and->condition($keys[$pos], $value);
+ }
+
+ $condition->condition($temp_and);
+ }
+
+ $this->query->condition($condition);
+ }
+ else {
+ $this->query->condition($keys[0], $simple_ids, 'IN');
+ }
}
else {
// 2. If the map is joinable, join it. We will want to accept all rows
@@ -325,8 +388,16 @@ class MigrateSourceSQL extends MigrateSource {
if ($condition_added) {
$this->query->condition($conditions);
}
+
+ // 4. Download data in batches for performance.
+ if ($this->batchSize > 0) {
+ $this->query->range($this->batch * $this->batchSize, $this->batchSize);
+ }
}
+ // Save our fixed-up query so getNextBatch() matches it.
+ $this->alteredQuery = clone $this->query;
+
migrate_instrument_start('MigrateSourceSQL execute');
$this->result = $this->query->execute();
migrate_instrument_stop('MigrateSourceSQL execute');
@@ -338,6 +409,71 @@ class MigrateSourceSQL extends MigrateSource {
* @return object
*/
public function getNextRow() {
- return $this->result->fetchObject();
+ $row = $this->result->fetchObject();
+
+ // We might be out of data entirely, or just out of data in the current batch.
+ // Attempt to fetch the next batch and see.
+ if (!is_object($row) && $this->batchSize > 0) {
+ $this->getNextBatch();
+ $row = $this->result->fetchObject();
+ }
+ if (is_object($row)) {
+ return $row;
+ }
+ else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Downloads the next set of data from the source database.
+ */
+ protected function getNextBatch() {
+ $this->batch++;
+ $query = clone $this->alteredQuery;
+ $query->range($this->batch * $this->batchSize, $this->batchSize);
+ $this->result = $query->execute();
+ }
+
+}
+
+/**
+ * Query extender for retrieving the connection used on the query.
+ */
+class MigrateConnectionQuery extends SelectQueryExtender {
+
+ public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
+ parent::__construct($query, $connection);
+ // Add the connection as metadata if anything else wants to access it.
+ $query->addMetaData('connection', $connection);
+ }
+
+ /**
+ * Return a string representing the source query.
+ *
+ * This is copied from devel module's dpq() function.
+ *
+ * @param bool $prefix
+ * If the tables should be prefixed. If FALSE will return tables names in
+ * the query like {tablename}.
+ *
+ * @return string
+ * The SQL query.
+ */
+ public function getString($prefix = TRUE) {
+ $query = $this;
+ if (method_exists($this, 'preExecute')) {
+ $query->preExecute();
+ }
+ $sql = (string) $this;
+ $quoted = array();
+ foreach ((array) $this->arguments() as $key => $val) {
+ $quoted[$key] = $this->connection->quote($val);
+ }
+ $sql = strtr($sql, $quoted);
+ if ($prefix) {
+ $sql = $this->connection->prefixTables($sql);
+ }
+ return $sql;
}
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/sqlmap.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/sqlmap.inc
index 6663231b..0ee2d102 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/sqlmap.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/sqlmap.inc
@@ -69,19 +69,40 @@ class MigrateSQLMap extends MigrateMap {
*/
protected $ensured;
+ /**
+ * Constructor.
+ *
+ * @param string $machine_name
+ * The unique reference to the migration that we are mapping.
+ * @param array $source_key
+ * The database schema for the source key.
+ * @param array $destination_key
+ * The database schema for the destination key.
+ * @param string $connection_key
+ * Optional - The connection used to create the mapping tables. By default
+ * this is the destination (Drupal). If it's not possible to make joins
+ * between the destination database and your source database you can specify
+ * a different connection to create the mapping tables on.
+ * @param array $options
+ * Optional - Options applied to this source.
+ */
public function __construct($machine_name, array $source_key,
array $destination_key, $connection_key = 'default', $options = array()) {
if (isset($options['track_last_imported'])) {
$this->trackLastImported = TRUE;
}
+
+ $this->connection = Database::getConnection('default', $connection_key);
+
// Default generated table names, limited to 63 characters
+ $prefixLength = strlen($this->connection->tablePrefix()) ;
$this->mapTable = 'migrate_map_' . drupal_strtolower($machine_name);
- $this->mapTable = drupal_substr($this->mapTable, 0, 63);
+ $this->mapTable = drupal_substr($this->mapTable, 0, 63 - $prefixLength);
$this->messageTable = 'migrate_message_' . drupal_strtolower($machine_name);
- $this->messageTable = drupal_substr($this->messageTable, 0, 63);
+ $this->messageTable = drupal_substr($this->messageTable, 0, 63 - $prefixLength);
$this->sourceKey = $source_key;
$this->destinationKey = $destination_key;
- $this->connection = Database::getConnection('default', $connection_key);
+
// Build the source and destination key maps
$this->sourceKeyMap = array();
$count = 1;
@@ -147,6 +168,12 @@ class MigrateSQLMap extends MigrateMap {
'default' => 0,
'description' => 'UNIX timestamp of the last time this row was imported',
);
+ $fields['hash'] = array(
+ 'type' => 'varchar',
+ 'length' => '32',
+ 'not null' => FALSE,
+ 'description' => 'Hash of source row data, for detecting changes',
+ );
$schema = array(
'description' => t('Mappings from source key to destination key'),
'fields' => $fields,
@@ -182,6 +209,29 @@ class MigrateSQLMap extends MigrateMap {
);
$this->connection->schema()->createTable($this->messageTable, $schema);
}
+ else {
+ // Add any missing columns to the map table
+ if (!$this->connection->schema()->fieldExists($this->mapTable,
+ 'rollback_action')) {
+ $this->connection->schema()->addField($this->mapTable,
+ 'rollback_action', array(
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Flag indicating what to do for this item on rollback',
+ ));
+ }
+ if (!$this->connection->schema()->fieldExists($this->mapTable, 'hash')) {
+ $this->connection->schema()->addField($this->mapTable, 'hash', array(
+ 'type' => 'varchar',
+ 'length' => '32',
+ 'not null' => FALSE,
+ 'description' => 'Hash of source row data, for detecting changes',
+ ));
+ }
+ }
$this->ensured = TRUE;
}
}
@@ -300,9 +350,12 @@ class MigrateSQLMap extends MigrateMap {
* @param int $rollback_action
* How to handle the destination object on rollback. Defaults to
* ROLLBACK_DELETE.
+ * $param string $hash
+ * If hashing is enabled, the hash of the raw source row.
*/
public function saveIDMapping(stdClass $source_row, array $dest_ids,
- $needs_update = MigrateMap::STATUS_IMPORTED, $rollback_action = MigrateMap::ROLLBACK_DELETE) {
+ $needs_update = MigrateMap::STATUS_IMPORTED,
+ $rollback_action = MigrateMap::ROLLBACK_DELETE, $hash = NULL) {
migrate_instrument_start('saveIDMapping');
// Construct the source key
$keys = array();
@@ -321,6 +374,7 @@ class MigrateSQLMap extends MigrateMap {
$fields = array(
'needs_update' => (int)$needs_update,
'rollback_action' => (int)$rollback_action,
+ 'hash' => $hash,
);
$count = 1;
if (!empty($dest_ids)) {
@@ -354,8 +408,8 @@ class MigrateSQLMap extends MigrateMap {
if (is_array($source_key)) {
foreach ($source_key as $key_value) {
$fields['sourceid' . $count++] = $key_value;
- // If any key value is empty, we can't save - print out and abort
- if (empty($key_value)) {
+ // If any key value is not set, we can't save - print out and abort
+ if (!isset($key_value)) {
print($message);
return;
}
diff --git a/sites/all/modules/contrib/migrate/migrate/plugins/sources/xml.inc b/sites/all/modules/contrib/migrate/migrate/plugins/sources/xml.inc
index c6df71b3..ede9f820 100644
--- a/sites/all/modules/contrib/migrate/migrate/plugins/sources/xml.inc
+++ b/sites/all/modules/contrib/migrate/migrate/plugins/sources/xml.inc
@@ -17,9 +17,9 @@
* example.
*/
-/* =========================================================================== */
-/* List Method */
-/* =========================================================================== */
+/* ========================================================================== */
+/* List Method */
+/* ========================================================================== */
/**
* Implementation of MigrateList, for retrieving a list of IDs to be migrated
* from an XML document.
@@ -32,32 +32,45 @@ class MigrateListXML extends MigrateList {
*/
protected $listUrl;
- public function __construct($list_url) {
+ /**
+ * An array of namespaces to explicitly register before Xpath queries.
+ *
+ * @var array
+ */
+ protected $namespaces;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($list_url, array $namespaces = array()) {
parent::__construct();
$this->listUrl = $list_url;
- // Suppress errors during parsing, so we can pick them up after
+ $this->namespaces = $namespaces;
+ // Suppress errors during parsing, so we can pick them up after.
libxml_use_internal_errors(TRUE);
}
/**
- * Our public face is the URL we're getting items from
+ * {@inheritdoc}
*
- * @return string
+ * Our public face is the URL we're getting items from
*/
public function __toString() {
return $this->listUrl;
}
/**
- * Load the XML at the given URL, and return an array of the IDs found within it.
+ * {@inheritdoc}
*
- * @return array
+ * Load the XML at the given URL, and return an array of the IDs found
+ * within it.
*/
public function getIdList() {
migrate_instrument_start("Retrieve $this->listUrl");
$xml = simplexml_load_file($this->listUrl);
migrate_instrument_stop("Retrieve $this->listUrl");
if ($xml !== FALSE) {
+ $this->registerNamespaces($xml);
return $this->getIDsFromXML($xml);
}
else {
@@ -73,35 +86,70 @@ class MigrateListXML extends MigrateList {
}
/**
+ * Gets an array of the IDs found in a XML.
+ *
* Given an XML object, parse out the IDs for processing and return them as an
* array. The default implementation assumes the IDs are simply the values of
* the top-level elements - in most cases, you will need to override this to
* reflect your particular XML structure.
*
* @param SimpleXMLElement $xml
+ * Object from we get the ID's
*
* @return array
+ * Extracted ID's
*/
protected function getIDsFromXML(SimpleXMLElement $xml) {
$ids = array();
foreach ($xml as $element) {
- $ids[] = (string)$element;
+ $ids[] = (string) $element;
+ }
+ // Additionally, if there are any namespaces registered, try to parse
+ // elements with namespaces as well.
+ if ($namespaces = $xml->getNamespaces()) {
+ foreach ($namespaces as $prefix => $url) {
+ foreach ($xml->children($url) as $element) {
+ $ids[] = (string) $element;
+ }
+ }
}
return array_unique($ids);
}
/**
- * Return a count of all available IDs from the source listing. The default
- * implementation assumes the count of top-level elements reflects the number
- * of IDs available - in many cases, you will need to override this to reflect
- * your particular XML structure.
+ * {@inheritdoc}
+ *
+ * Return a count of all available IDs from the source listing.
+ * The default implementation assumes the count of top-level elements
+ * reflects the number of IDs available - in many cases, you will need
+ * to override this to reflect your particular XML structure.
*/
public function computeCount() {
$xml = simplexml_load_file($this->listUrl);
- // Number of sourceid elements beneath the top-level element
+ $this->registerNamespaces($xml);
+ // Number of sourceid elements beneath the top-level element.
$count = count($xml);
+ // Additionally, if there are any namespaces registered, try to count
+ // elements with namespaces as well.
+ if ($namespaces = $xml->getNamespaces()) {
+ foreach ($namespaces as $prefix => $url) {
+ $count += count($xml->children($url));
+ }
+ }
return $count;
}
+
+ /**
+ * Explicitly register namespaces on an XML element.
+ *
+ * @param SimpleXMLElement $xml
+ * A SimpleXMLElement to register the namespaces on.
+ */
+ protected function registerNamespaces(SimpleXMLElement &$xml) {
+ foreach ($this->namespaces as $prefix => $namespace) {
+ $xml->registerXPathNamespace($prefix, $namespace);
+ }
+ }
}
/**
@@ -117,40 +165,62 @@ class MigrateItemXML extends MigrateItem {
*/
protected $itemUrl;
- public function __construct($item_url) {
+ /**
+ * An array of namespaces to explicitly register before Xpath queries.
+ *
+ * @var array
+ */
+ protected $namespaces;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($item_url, array $namespaces = array()) {
parent::__construct();
$this->itemUrl = $item_url;
- // Suppress errors during parsing, so we can pick them up after
+ $this->namespaces = $namespaces;
+ // Suppress errors during parsing, so we can pick them up after.
libxml_use_internal_errors(TRUE);
}
/**
+ * Explicitly register namespaces on an XML element.
+ *
+ * @param SimpleXMLElement $xml
+ * A SimpleXMLElement to register the namespaces on.
+ */
+ protected function registerNamespaces(SimpleXMLElement &$xml) {
+ foreach ($this->namespaces as $prefix => $namespace) {
+ $xml->registerXPathNamespace($prefix, $namespace);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
* Implementors are expected to return an object representing a source item.
- *
- * @param mixed $id
- *
- * @return stdClass
*/
public function getItem($id) {
- // Make sure we actually have an ID
+ // Make sure we actually have an ID.
if (empty($id)) {
return NULL;
}
$item_url = $this->constructItemUrl($id);
- // And make sure we actually got a URL to fetch
+ // And make sure we actually got a URL to fetch.
if (empty($item_url)) {
return NULL;
}
- // Get the XML object at the specified URL;
+ // Get the XML object at the specified URL.
$xml = $this->loadXmlUrl($item_url);
if ($xml !== FALSE) {
- $return = new stdclass;
+ $this->registerNamespaces($xml);
+ $return = new stdclass();
$return->xml = $xml;
return $return;
}
else {
$migration = Migration::currentMigration();
- $message = t('Loading of !objecturl failed:', array('!objecturl' => $item_url));
+ $message = t('Loading of !objecturl failed:', array('!objecturl' => $item_url));
foreach (libxml_get_errors() as $error) {
$message .= "\n" . $error->message;
}
@@ -162,24 +232,50 @@ class MigrateItemXML extends MigrateItem {
}
/**
+ * Creates a valid URL pointing to current item.
+ *
* The default implementation simply replaces the :id token in the URL with
- * the ID obtained from MigrateListXML. Override if the item URL is not
- * so easily expressed from the ID.
+ * the ID obtained from MigrateListXML. Override if the item URL is not so
+ * easily expressed from the ID.
*
* @param mixed $id
+ * XML item ID
+ *
+ * @return string
+ * Formatted string with replaced tokens
*/
protected function constructItemUrl($id) {
return str_replace(':id', $id, $this->itemUrl);
}
/**
- * Default XML loader - just use Simplexml directly. This can be overridden for
- * preprocessing of XML (removal of unwanted elements, caching of XML if the
- * source service is slow, etc.)
+ * Loads the XML.
+ *
+ * Default XML loader - just use Simplexml directly. This can be overridden
+ * for preprocessing of XML (removal of unwanted elements, caching of XML if
+ * the source service is slow, etc.)
+ *
+ * @param string $item_url
+ * URL to the XML file
+ *
+ * @return SimpleXMLElement
+ * Loaded XML
*/
protected function loadXmlUrl($item_url) {
return simplexml_load_file($item_url);
}
+
+ /**
+ * Implments MigrateItem::hash().
+ */
+ public function hash($row) {
+ // $row->xml is a SimpleXMLElement. Temporarily set it as an XML string
+ // to prevent parent::hash() failing when try to create the hash.
+ migrate_instrument_start('MigrateItemXML::hash');
+ $hash = md5(serialize($row->xml->asXML()));
+ migrate_instrument_stop('MigrateItemXML::hash');
+ return $hash;
+ }
}
/**
@@ -192,14 +288,22 @@ class MigrateXMLFieldMapping extends MigrateFieldMapping {
* @var string
*/
protected $xpath;
+
+ /**
+ * Get xpath of current item.
+ */
public function getXpath() {
return $this->xpath;
}
/**
- * Add an xpath to this field mapping
+ * Add an xpath to this field mapping.
*
* @param string $xpath
+ * xpath
+ *
+ * @return MigrateFieldMapping
+ * MigrateFieldMapping
*/
public function xpath($xpath) {
$this->xpath = $xpath;
@@ -212,22 +316,27 @@ class MigrateXMLFieldMapping extends MigrateFieldMapping {
*/
abstract class XMLMigration extends Migration {
/**
- * Override the default addFieldMapping(), so we can create our special
- * field mapping class.
- * TODO: Find a cleaner way to just substitute a different mapping class
+ * {@inheritdoc}
*
- * @param string $destinationField
- * Name of the destination field.
- * @param string $sourceField
- * Name of the source field (optional).
- * @param boolean $warn_on_override
- * Set to FALSE to prevent warnings when there's an existing mapping
- * for this destination field.
+ * So we can create our special field mapping class.
+ *
+ * @todo Find a cleaner way to just substitute a different mapping class.
+ *
+ * @param string|null $destination_field
+ * machine-name of destination field
+ * @param string|null $source_field
+ * name of source field
+ * @param bool $warn_on_override
+ * Set to FALSE to prevent warnings when there's an existing mapping
+ * for this destination field.
+ *
+ * @return MigrateXMLFieldMapping
+ * MigrateXMLFieldMapping
*/
public function addFieldMapping($destination_field, $source_field = NULL,
$warn_on_override = TRUE) {
- // Warn of duplicate mappings
- if ($warn_on_override && !is_null($destination_field) && isset($this->fieldMappings[$destination_field])) {
+ // Warn of duplicate mappings.
+ if ($warn_on_override && !is_null($destination_field) && isset($this->codedFieldMappings[$destination_field])) {
self::displayMessage(
t('!name addFieldMapping: !dest was previously mapped, overridden',
array('!name' => $this->machineName, '!dest' => $destination_field)),
@@ -235,15 +344,17 @@ abstract class XMLMigration extends Migration {
}
$mapping = new MigrateXMLFieldMapping($destination_field, $source_field);
if (is_null($destination_field)) {
- $this->fieldMappings[] = $mapping;
+ $this->codedFieldMappings[] = $mapping;
}
else {
- $this->fieldMappings[$destination_field] = $mapping;
+ $this->codedFieldMappings[$destination_field] = $mapping;
}
return $mapping;
}
/**
+ * {@inheritdoc}
+ *
* A normal $data_row has all the input data as top-level fields - in this
* case, however, the data is embedded within a SimpleXMLElement object in
* $data_row->xml. Explode that out to the normal form, and pass on to the
@@ -251,13 +362,16 @@ abstract class XMLMigration extends Migration {
*/
protected function applyMappings() {
// We only know what data to pull from the xpaths in the mappings.
- foreach ($this->fieldMappings as $mapping) {
+ foreach ($this->getFieldMappings() as $mapping) {
$source = $mapping->getSourceField();
- if ($source) {
+ if ($source && !isset($this->sourceValues->{$source})) {
$xpath = $mapping->getXpath();
if ($xpath) {
- // Derived class may override applyXpath()
- $this->sourceValues->$source = $this->applyXpath($this->sourceValues, $xpath);
+ // Derived class may override applyXpath().
+ $source_value = $this->applyXpath($this->sourceValues, $xpath);
+ if (!is_null($source_value)) {
+ $this->sourceValues->$source = $source_value;
+ }
}
}
}
@@ -265,10 +379,17 @@ abstract class XMLMigration extends Migration {
}
/**
+ * Gets item from XML using the xpath.
+ *
* Default implementation - straightforward xpath application
*
- * @param $data_row
- * @param $xpath
+ * @param stdClass $data_row
+ * row containing items.
+ * @param string $xpath
+ * xpath used to find the item
+ *
+ * @return SimpleXMLElement
+ * found element
*/
public function applyXpath($data_row, $xpath) {
$result = $data_row->xml->xpath($xpath);
@@ -276,12 +397,12 @@ abstract class XMLMigration extends Migration {
if (count($result) > 1) {
$return = array();
foreach ($result as $record) {
- $return[] = (string)$record;
+ $return[] = (string) $record;
}
return $return;
}
else {
- return (string)$result[0];
+ return (string) $result[0];
}
}
else {
@@ -290,32 +411,70 @@ abstract class XMLMigration extends Migration {
}
}
-/* =========================================================================== */
-/* MultiItems Method */
-/* =========================================================================== */
+/* ========================================================================== */
+/* MultiItems Method */
+/* ========================================================================== */
/**
* Implementation of MigrateItems, for providing a list of IDs and for
* retrieving a parsed XML document given an ID from this list.
*/
class MigrateItemsXML extends MigrateItems {
/**
- * A URL pointing to an XML document containing the ids and data.
+ * An array with all urls to available xml files.
+ *
+ * @var array
+ */
+ protected $urls;
+
+ /**
+ * Define the current cursor over the urls array.
*
* @var string
*/
- protected $xmlUrl;
+ protected $currentUrl;
/**
- * Stores the loaded XML document.
+ * An array of namespaces to explicitly register before Xpath queries.
+ *
+ * @var array
+ */
+ protected $namespaces;
+
+ /**
+ * Stores the loaded XML document from currentUrl.
*
* @var SimpleXMLElement
*/
- protected $xml = FALSE;
+ protected $currentXml = FALSE;
/**
- * xpath identifying the element used for each item
+ * To find the right url depending on the id, we'll build a map in the form of
+ * an array('url1' => $ids, 'url2' => $ids, ...).
+ *
+ * @var array
+ */
+ protected $idsMap = NULL;
+
+ /**
+ * Stores the id list from all urls.
+ *
+ * @var array
+ */
+ protected $cacheIDs = NULL;
+
+ /**
+ * xpath identifying the element used for each item.
+ *
+ * @var string
*/
protected $itemXpath;
+
+ /**
+ * Gets xpath identifying the element used for each item.
+ *
+ * @return string
+ * xpath
+ */
public function getItemXpath() {
return $this->itemXpath;
}
@@ -323,56 +482,98 @@ class MigrateItemsXML extends MigrateItems {
/**
* xpath identifying the subelement under itemXpath that holds the id for
* each item.
+ *
+ * @var string
*/
protected $itemIDXpath;
+
+ /**
+ * Getter for itemIDXpath.
+ *
+ * @return string
+ */
public function getIDXpath() {
return $this->itemIDXpath;
}
- public function __construct($xml_url, $item_xpath='item', $itemID_xpath='id') {
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($urls, $item_xpath = 'item', $item_id_xpath = 'id',
+ array $namespaces = array()) {
parent::__construct();
- $this->xmlUrl = $xml_url;
+ if (!is_array($urls)) {
+ $urls = array($urls);
+ }
+ $this->urls = $urls;
$this->itemXpath = $item_xpath;
- $this->itemIDXpath = $itemID_xpath;
+ $this->itemIDXpath = $item_id_xpath;
+ $this->namespaces = $namespaces;
- // Suppress errors during parsing, so we can pick them up after
+ // Suppress errors during parsing, so we can pick them up after.
libxml_use_internal_errors(TRUE);
}
/**
- * Our public face is the URL we're getting items from
+ * Explicitly register namespaces on an XML element.
*
- * @return string
+ * @param SimpleXMLElement $xml
+ * A SimpleXMLElement to register the namespaces on.
*/
- public function __toString() {
- return 'url = ' . $this->xmlUrl . ' | item xpath = ' . $this->itemXpath .
- ' | item ID xpath = ' . $this->itemIDXpath;
+ protected function registerNamespaces(SimpleXMLElement &$xml) {
+ foreach ($this->namespaces as $prefix => $namespace) {
+ $xml->registerXPathNamespace($prefix, $namespace);
+ }
}
/**
- * Load and return the xml from the defined xmlUrl.
+ * Our public face is the URL list we're getting items from.
+ */
+ public function __toString() {
+ $urls = implode('- ', $this->urls);
+ // Prepare a list of urls.
+ $output = 'urls =
- ' . $urls . '
';
+ $output .= '
';
+ // Add selection rules to the end.
+ $output .= 'item xpath = ' . $this->itemXpath . ' | ';
+ $output .= 'item ID xpath = ' . $this->itemIDXpath;
+
+ return $output;
+ }
+
+ /**
+ * Load and return the xml from currentUrl.
+ *
* @return SimpleXMLElement
+ * SimpleXMLElement
*/
public function &xml() {
- if (!$this->xml && !empty($this->xmlUrl)) {
- $this->xml = simplexml_load_file($this->xmlUrl);
- if (!$this->xml) {
+ if (!empty($this->currentUrl)) {
+ $this->currentXml = simplexml_load_file($this->currentUrl);
+ if ($this->currentXml === FALSE) {
Migration::displayMessage(t(
- 'Loading of !xmlUrl failed:',
- array('!xmlUrl' => $this->xmlUrl)
+ 'Loading of !currentUrl failed:',
+ array('!currentUrl' => $this->currentUrl)
));
foreach (libxml_get_errors() as $error) {
Migration::displayMessage(self::parseLibXMLError($error));
}
}
+ else {
+ $this->registerNamespaces($this->currentXml);
+ }
}
- return $this->xml;
+ return $this->currentXml;
}
/**
* Parses a LibXMLError to a error message string.
+ *
* @param LibXMLError $error
+ * Error thrown by the XML
+ *
* @return string
+ * Error message
*/
public static function parseLibXMLError(LibXMLError $error) {
$error_code_name = 'Unknown Error';
@@ -380,15 +581,18 @@ class MigrateItemsXML extends MigrateItems {
case LIBXML_ERR_WARNING:
$error_code_name = t('Warning');
break;
+
case LIBXML_ERR_ERROR:
$error_code_name = t('Error');
break;
+
case LIBXML_ERR_FATAL:
$error_code_name = t('Fatal Error');
break;
}
+
return t(
- "!libxmlerrorcodename !libxmlerrorcode: !libxmlerrormessage\n" .
+ "!libxmlerrorcodename !libxmlerrorcode: !libxmlerrormessage\n" .
"Line: !libxmlerrorline\n" .
"Column: !libxmlerrorcolumn\n" .
"File: !libxmlerrorfile",
@@ -404,17 +608,33 @@ class MigrateItemsXML extends MigrateItems {
}
/**
- * Load the XML at the given URL, and return an array of the IDs found
- * within it.
+ * Load ID's from URLs.
+ *
+ * Load ids from all urls and map them in idsMap depending on the currentURL.
+ *
+ * After ids were fetched from all urls store them in cacheIDs and return the
+ * whole list.
*
* @return array
+ * mapped ID's
*/
public function getIdList() {
- migrate_instrument_start("Retrieve $this->xmlUrl");
- $xml = $this->xml();
- migrate_instrument_stop("Retrieve $this->xmlUrl");
- if ($xml !== FALSE) {
- return $this->getIDsFromXML($xml);
+ $ids = array();
+ foreach ($this->urls as $url) {
+ migrate_instrument_start("Retrieve $url");
+ // Make sure, to load new xml.
+ $this->currentUrl = $url;
+ $xml = $this->xml();
+ if ($xml !== FALSE) {
+ $url_ids = $this->getIdsFromXML($xml);
+ $this->idsMap[$url] = $url_ids;
+ $ids = array_merge($ids, $url_ids);
+ }
+ migrate_instrument_stop("Retrieve $url");
+ }
+ if (!empty($ids)) {
+ $this->cacheIDs = array_unique($ids);
+ return $this->cacheIDs;
}
return NULL;
}
@@ -429,93 +649,103 @@ class MigrateItemsXML extends MigrateItems {
* TRUE.
*
* @param SimpleXMLElement $xml
- * @param boolean $refresh
+ * SimpleXMLElement
*
* @return array
*/
- protected $cache_ids = NULL;
- protected function getIDsFromXML(SimpleXMLElement $xml, $refresh = FALSE) {
- if ($refresh !== TRUE && $this->cache_ids != NULL) {
- return $this->cache_ids;
- }
-
- $this->cache_ids = NULL;
+ protected function getIDsFromXML(SimpleXMLElement $xml) {
$result = $xml->xpath($this->itemXpath);
$ids = array();
if ($result) {
foreach ($result as $element) {
+ if (!isset($element)) {
+ continue;
+ }
+ // Namespaces must be reapplied after xpath().
+ $this->registerNamespaces($element);
$id = $this->getItemID($element);
if (!is_null($id)) {
- $ids[] = (string)$id;
+ $ids[] = (string) $id;
}
}
}
- $this->cache_ids = array_unique($ids);
- return $this->cache_ids;
+ return array_unique($ids);
}
+
/**
* Return a count of all available IDs from the source listing.
+ *
+ * @return int
+ * count of available IDs
*/
public function computeCount() {
- $count = 0;
- $xml = $this->xml();
- if ($xml !== FALSE) {
- $ids = $this->getIDsFromXML($xml, TRUE);
- $count = count($ids);
+ if (!isset($this->cacheIDs)) {
+ $this->getIdList();
}
- return $count;
+ return count($this->cacheIDs);
}
/**
- * Load the XML at the given URL, and return an array of the Items found
- * within it.
+ * Load the XML at the given URL, and return an array.
*
* @return array
+ * array of the Items found within it.
*/
public function getAllItems() {
$xml = $this->xml();
if ($xml !== FALSE) {
- return $this->getItemsFromXML($xml);
+ return $this->getItemsFromXML($xml, TRUE);
}
return NULL;
}
+ protected $currentItems = NULL;
+
/**
+ * Parses out the items from a given XML object, and parse it's items.
+ *
* Given an XML object, parse out the items for processing and return them as
* an array. The location of the items in the XML are based on the item xpath
- * set in the constructor. Items are cached. The list of items are returned
- * from the cache except when this is the first call (ie, cache is NULL) OR
- * the refresh parameter is TRUE.
+ * set in the constructor. Items from currentUrl are cached. The list of items
+ * returned from the cache except when this is the first call
+ * (ie, cache is NULL) OR the refresh parameter is TRUE.
*
- * Items are cached as an array of key=ID and value=stdclass object with
+ * Items are cached as an array of key=ID and value=stdClass object with
* attribute xml containing the xml SimpleXMLElement object of the item.
*
* @param SimpleXMLElement $xml
- * @param boolean $refresh
+ * XML to parse
+ * @param bool $refresh
+ * Indicates if necessary parse again the items or get them from cache.
*
* @return array
+ * Array of obtained items.
*/
- protected $cache_items = NULL;
- public function getItemsFromXML(SimpleXMLElement $xml, $refresh=FALSE) {
- if ($refresh !== FALSE && $this->cache_items != NULL) {
- return $this->cache_items;
+ public function getItemsFromXML(SimpleXMLElement $xml, $refresh = FALSE) {
+ if ($refresh !== FALSE && $this->currentItems != NULL) {
+ return $this->currentItems;
}
- $this->cache_items = NULL;
+ $this->currentItems = NULL;
$items = array();
$result = $xml->xpath($this->itemXpath);
if ($result) {
foreach ($result as $item_xml) {
+ if (!isset($item_xml)) {
+ continue;
+ }
+ // Namespaces must be reapplied after xpath().
+ $this->registerNamespaces($item_xml);
$id = $this->getItemID($item_xml);
- $item = new stdclass;
+ $item = new stdclass();
$item->xml = $item_xml;
$items[$id] = $item;
}
- $this->cache_items = $items;
- return $items;
+ $this->currentItems = $items;
+ return $this->currentItems;
}
else {
return NULL;
@@ -525,49 +755,78 @@ class MigrateItemsXML extends MigrateItems {
/**
* Get the item ID from the itemXML based on itemIDXpath.
*
+ * @param SimpleXMLElement $item_xml
+ * Element from we get the ID
+ *
* @return string
+ * The item ID
*/
- protected function getItemID($itemXML) {
- return $this->getElementValue($itemXML, $this->itemIDXpath);
+ protected function getItemID($item_xml) {
+ return $this->getElementValue($item_xml, $this->itemIDXpath);
}
/**
* Get an element from the itemXML based on an xpath.
*
+ * @param SimpleXMLElement $item_xml
+ * Element from we get the required value
+ * @param string $xpath
+ * xpath used to locate the value
+ *
* @return string
+ * Extracted value
*/
- protected function getElementValue($itemXML, $xpath) {
+ protected function getElementValue($item_xml, $xpath) {
$value = NULL;
- if ($itemXML) {
- $result = $itemXML->xpath($xpath);
- if ($result)
- $value = (string)$result[0];
+ if ($item_xml->asXML()) {
+ $result = $item_xml->xpath($xpath);
+ if ($result) {
+ $value = (string) $result[0];
+ }
}
return $value;
}
/**
- * Implementors are expected to return an object representing a source item.
- * Items are cached as an array of key=ID and value=stdclass object with
- * attribute xml containing the xml SimpleXMLElement object of the item.
+ * Implementers are expected to return an object representing a source item.
+ * Items from currentUrl are cached as an array of key=ID and value=stdClass
+ * object with attribute xml containing the xml SimpleXMLElement object of the
+ * item.
*
* @param mixed $id
*
* @return stdClass
*/
public function getItem($id) {
- // Make sure we actually have an ID
+ // Make sure we actually have an ID.
if (empty($id)) {
return NULL;
}
- $items = $this->getAllItems();
- $item = $items[$id];
- if ($item) {
+ // If $id is in currentXml return the right item immediately.
+ if (isset($this->currentItems) && isset($this->currentItems[$id])) {
+ $item = $this->currentItems[$id];
+ }
+ else {
+ // Otherwise find the right url and get the items from.
+ if ($this->idsMap === NULL) {
+ // Populate the map.
+ $this->getIdList();
+ }
+ foreach ($this->idsMap as $url => $ids) {
+ if (in_array($id, $ids)) {
+ $this->currentItems = NULL;
+ $this->currentUrl = $url;
+ $items = $this->getAllItems();
+ $item = $items[$id];
+ }
+ }
+ }
+ if (!empty($item)) {
return $item;
}
else {
$migration = Migration::currentMigration();
- $message = t('Loading of item XML for ID !id failed:', array('!id' => $id));
+ $message = t('Loading of item XML for ID !id failed:', array('!id' => $id));
foreach (libxml_get_errors() as $error) {
$message .= "\n" . $error->message;
}
@@ -577,6 +836,18 @@ class MigrateItemsXML extends MigrateItems {
return NULL;
}
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hash($row) {
+ // $row->xml is a SimpleXMLElement. Temporarily set it as an XML string
+ // to prevent parent::hash() failing when try to create the hash.
+ migrate_instrument_start('MigrateItemXML::hash');
+ $hash = md5(serialize($row->xml->asXML()));
+ migrate_instrument_stop('MigrateItemXML::hash');
+ return $hash;
+ }
}
/**
@@ -642,7 +913,8 @@ class MigrateXMLReader implements Iterator {
public $elementQuery;
/**
- * Xpath query string used to retrieve the primary key value from each element.
+ * Xpath query string used to retrieve the primary key value from each
+ * element.
*
* @var string
*/
@@ -673,26 +945,22 @@ class MigrateXMLReader implements Iterator {
/**
* Prepares our extensions to the XMLReader object.
*
- * @param $xml_url
- * URL of the XML file to be parsed.
- * @param $element_query
- * Query string in a restricted xpath format, for selecting elements to be
- * returned by the interator. Supported syntax:
- * - The full path to the element must be specified; i.e., /file/article
- * rather than //article.
- * - The elements may be filtered by attribute value by appending
- * [@attribute="value"].
- * @param $id_query
- * Query string to the unique identifier for an element, relative to the root
- * of that element. This supports the full xpath syntax.
+ * @param string $xml_url
+ * URL of the XML file to be parsed.
+ * @param string $element_query
+ * Query string in a restricted xpath format, for selecting elements to be
+ * @param string $id_query
+ * Query string to the unique identifier for an element,
+ * relative to the root of that element. This supports the full
+ * xpath syntax.
*/
public function __construct($xml_url, $element_query, $id_query) {
- $this->reader = new XMLReader;
+ $this->reader = new XMLReader();
$this->url = $xml_url;
$this->elementQuery = $element_query;
$this->idQuery = $id_query;
- // Suppress errors during parsing, so we can pick them up after
+ // Suppress errors during parsing, so we can pick them up after.
libxml_use_internal_errors(TRUE);
// Parse the element query. First capture group is the element path, second
@@ -703,13 +971,15 @@ class MigrateXMLReader implements Iterator {
$attribute_query = $matches[2][0];
if ($attribute_query) {
// Matches [@attribute="value"] (with either single- or double-quotes).
- preg_match_all('|^\[@([^=]+)=[\'"](.*)[\'"]\]$|', $attribute_query, $matches);
+ preg_match_all('|^\[@([^=]+)=[\'"](.*)[\'"]\]$|', $attribute_query,
+ $matches);
$this->attributeName = $matches[1][0];
$this->attributeValue = $matches[2][0];
}
- // If the element path contains any colons, it must be specifying namespaces,
- // so we need to compare using the prefixed element name in next().
+ // If the element path contains any colons, it must be specifying
+ // namespaces, so we need to compare using the prefixed element
+ // name in next().
if (strpos($element_path, ':')) {
$this->prefixedName = TRUE;
}
@@ -717,29 +987,27 @@ class MigrateXMLReader implements Iterator {
/**
* Implementation of Iterator::rewind().
- *
- * @return void
*/
public function rewind() {
// (Re)open the provided URL.
$this->reader->close();
- $status = $this->reader->open($this->url);
- if (!$status) {
+ $status = $this->reader->open($this->url, NULL, LIBXML_NOWARNING);
+
+ // Reset our path tracker.
+ $this->currentPath = array();
+
+ if ($status) {
+ // Load the first matching element and its ID.
+ $this->next();
+ }
+ else {
Migration::displayMessage(t('Could not open XML file !url',
array('!url' => $this->url)));
}
-
- // Reset our path tracker
- $this->currentPath = array();
-
- // Load the first matching element and its ID.
- $this->next();
}
/**
* Implementation of Iterator::next().
- *
- * @return void
*/
public function next() {
migrate_instrument_start('MigrateXMLReader::next');
@@ -759,9 +1027,12 @@ class MigrateXMLReader implements Iterator {
// We're positioned to the right element path - if filtering on an
// attribute, check that as well before accepting this element.
if (empty($this->attributeName) ||
- ($this->reader->getAttribute($this->attributeName) == $this->attributeValue)) {
- // We've found a matching element - get a SimpleXML object representing it.
- // We must associate the DOMNode with a DOMDocument to be able to import
+ ($this->reader->getAttribute($this->attributeName) ==
+ $this->attributeValue)
+ ) {
+ // We've found a matching element - get a SimpleXML object
+ // representing it.We must associate the DOMNode with a
+ // DOMDocument to be able to import
// it into SimpleXML.
// Despite appearances, this is almost twice as fast as
// simplexml_load_string($this->readOuterXML());
@@ -773,7 +1044,7 @@ class MigrateXMLReader implements Iterator {
$this->currentElement = simplexml_import_dom($node);
$idnode = $this->currentElement->xpath($this->idQuery);
if (is_array($idnode)) {
- $this->currentId = (string)reset($idnode);
+ $this->currentId = (string) reset($idnode);
}
else {
throw new Exception(t('Failure retrieving ID, xpath: !xpath',
@@ -796,7 +1067,7 @@ class MigrateXMLReader implements Iterator {
}
}
elseif ($this->reader->nodeType == XMLREADER::END_ELEMENT) {
- // Remove this element and any deeper ones from the current path
+ // Remove this element and any deeper ones from the current path.
foreach ($this->currentPath as $depth => $name) {
if ($depth >= $this->reader->depth) {
unset($this->currentPath[$depth]);
@@ -811,6 +1082,7 @@ class MigrateXMLReader implements Iterator {
* Implementation of Iterator::current().
*
* @return null|SimpleXMLElement
+ * Current item
*/
public function current() {
return $this->currentElement;
@@ -820,6 +1092,7 @@ class MigrateXMLReader implements Iterator {
* Implementation of Iterator::key().
*
* @return null|string
+ * Current key
*/
public function key() {
return $this->currentId;
@@ -829,9 +1102,10 @@ class MigrateXMLReader implements Iterator {
* Implementation of Iterator::valid().
*
* @return bool
+ * Indicates if current element is valid
*/
public function valid() {
- return !empty($this->currentElement);
+ return $this->currentElement instanceof SimpleXMLElement;
}
}
@@ -841,12 +1115,20 @@ class MigrateXMLReader implements Iterator {
class MigrateSourceXML extends MigrateSource {
/**
- * The MigrateXMLReader object serving as a cursor over the XML source.
- *
- * @var MigrateXMLReader
+ * @var $reader MigrateXMLReader
*/
protected $reader;
+ /**
+ * The MigrateXMLReader object serving as a cursor over the XML source.
+ *
+ * @return MigrateXMLReader
+ * MigrateXMLReader
+ */
+ public function getReader() {
+ return $this->reader;
+ }
+
/**
* The source URLs to load XML from
*
@@ -861,6 +1143,13 @@ class MigrateSourceXML extends MigrateSource {
*/
protected $activeUrl = NULL;
+ /**
+ * An array of namespaces to explicitly register before Xpath queries.
+ *
+ * @var array
+ */
+ protected $namespaces;
+
/**
* Store the query string used to recognize elements being iterated
* so we can create reader objects on the fly.
@@ -895,25 +1184,26 @@ class MigrateSourceXML extends MigrateSource {
/**
* Source constructor.
*
- * @param string or array $url
- * URL(s) of the XML source data.
+ * @param string|array $urls
+ * URL(s) of the XML source data.
* @param string $element_query
- * Query string used to recognize elements being iterated.
+ * Query string used to recognize elements being iterated.
* @param string $id_query
- * Xpath query string used to retrieve the primary key value from each element.
+ * Xpath query string used to retrieve the primary key value
+ * from each element.
* @param array $fields
- * Optional - keys are field names, values are descriptions. Use to override
- * the default descriptions, or to add additional source fields which the
- * migration will add via other means (e.g., prepareRow()).
- * @param boolean $options
- * Options applied to this source. In addition to the standard MigrateSource
- * options, we support:
- * - reader_class: The reader class to instantiate for traversing the XML -
- * defaults to MigrateXMLReader (any substitutions must be derived from
- * MigrateXMLReader).
+ * Optional - keys are field names, values are descriptions. Use to override
+ * the default descriptions, or to add additional source fields which the
+ * migration will add via other means (e.g., prepareRow()).
+ * @param array $options
+ * Options applied to this source. In addition to the standard MigrateSource
+ * options, we support:
+ * - reader_class: The reader class to instantiate for traversing the XML -
+ * defaults to MigrateXMLReader (any substitutions must be derived from
+ * MigrateXMLReader).
*/
public function __construct($urls, $element_query, $id_query, array $fields = array(),
- array $options = array()) {
+ array $options = array(), array $namespaces = array()) {
parent::__construct($options);
if (empty($options['reader_class'])) {
$reader_class = 'MigrateXMLReader';
@@ -932,16 +1222,31 @@ class MigrateSourceXML extends MigrateSource {
$this->idQuery = $id_query;
$this->readerClass = $reader_class;
$this->fields = $fields;
+ $this->namespaces = $namespaces;
+ }
+
+ /**
+ * Explicitly register namespaces on an XML element.
+ *
+ * @param SimpleXMLElement $xml
+ * A SimpleXMLElement to register the namespaces on.
+ */
+ protected function registerNamespaces(SimpleXMLElement &$xml) {
+ foreach ($this->namespaces as $prefix => $namespace) {
+ $xml->registerXPathNamespace($prefix, $namespace);
+ }
}
/**
* Return a string representing the source query.
*
* @return string
+ * source query
*/
public function __toString() {
// Clump the urls into a string
- // This could cause a problem when using a lot of urls, may need to hash
+ // This could cause a problem when using
+ // a lot of urls, may need to hash.
$urls = implode(', ', $this->sourceUrls);
return 'urls = ' . $urls .
' | item xpath = ' . $this->elementQuery .
@@ -952,8 +1257,8 @@ class MigrateSourceXML extends MigrateSource {
* Returns a list of fields available to be mapped from the source query.
*
* @return array
- * Keys: machine names of the fields (to be passed to addFieldMapping)
- * Values: Human-friendly descriptions of the fields.
+ * keys: machine names of the fields (to be passed to addFieldMapping)
+ * values: Human-friendly descriptions of the fields.
*/
public function fields() {
return $this->fields;
@@ -963,9 +1268,10 @@ class MigrateSourceXML extends MigrateSource {
* Returns the active Url.
*
* @return string
+ * active Url
*/
public function activeUrl() {
- if ($this->activeUrl) {
+ if (!is_null($this->activeUrl)) {
return $this->sourceUrls[$this->activeUrl];
}
}
@@ -976,7 +1282,8 @@ class MigrateSourceXML extends MigrateSource {
public function computeCount() {
$count = 0;
foreach ($this->sourceUrls as $url) {
- $reader = new $this->readerClass($url, $this->elementQuery, $this->idQuery);
+ $reader = new $this->readerClass($url, $this->elementQuery,
+ $this->idQuery);
foreach ($reader as $element) {
$count++;
}
@@ -1000,7 +1307,7 @@ class MigrateSourceXML extends MigrateSource {
* Implementation of MigrationSource::getNextRow().
*
* @return stdClass
- * data for the next row from the XML source files
+ * data for the next row from the XML source files
*/
public function getNextRow() {
migrate_instrument_start('MigrateSourceXML::next');
@@ -1009,24 +1316,27 @@ class MigrateSourceXML extends MigrateSource {
$key_name = key($source_key);
$row = NULL;
- // The reader is now lazy loaded, so it may not be defined yet, need to test if set
+ // The reader is now lazy loaded, so it may
+ // not be defined yet, need to test if set.
if (isset($this->reader)) {
- // attempt to load the next row
+ // Attempt to load the next row.
$this->reader->next();
}
- // Test the reader for a valid row
+ // Test the reader for a valid row.
if (isset($this->reader) && $this->reader->valid()) {
- $row = new stdClass;
+ $row = new stdClass();
$row->$key_name = $this->reader->key();
$row->xml = $this->reader->current();
+ $this->registerNamespaces($row->xml);
}
else {
- // The current source is at the end, try to load the next source
+ // The current source is at the end, try to load the next source.
if ($this->getNextSource()) {
- $row = new stdClass;
+ $row = new stdClass();
$row->$key_name = $this->reader->key();
$row->xml = $this->reader->current();
+ $this->registerNamespaces($row->xml);
}
}
@@ -1035,31 +1345,33 @@ class MigrateSourceXML extends MigrateSource {
}
/**
- * Advances the reader to the next source from source_urls
+ * Advances the reader to the next source from source_urls.
*
* @return bool
- * TRUE if a valid source was loaded
+ * TRUE if a valid source was loaded
*/
public function getNextSource() {
migrate_instrument_start('MigrateSourceXML::nextSource');
- // Return value
+ // Return value.
$status = FALSE;
- while ($this->activeUrl === NULL || (count($this->sourceUrls)-1) > $this->activeUrl) {
+ while ($this->activeUrl === NULL || (count($this->sourceUrls) - 1) > $this->activeUrl) {
if (is_null($this->activeUrl)) {
$this->activeUrl = 0;
}
else {
- // Increment the activeUrl so we try to load the next source
+ // Increment the activeUrl so we try to load the next source.
$this->activeUrl = $this->activeUrl + 1;
}
- $this->reader = new $this->readerClass($this->sourceUrls[$this->activeUrl], $this->elementQuery, $this->idQuery);
+ $this->reader = new $this->readerClass(
+ $this->sourceUrls[$this->activeUrl], $this->elementQuery,
+ $this->idQuery);
$this->reader->rewind();
if ($this->reader->valid()) {
- // We have a valid source
+ // We have a valid source.
$status = TRUE;
break;
}
@@ -1068,4 +1380,13 @@ class MigrateSourceXML extends MigrateSource {
migrate_instrument_stop('MigrateSourceXML::nextSource');
return $status;
}
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function hash($row) {
+ // $row->xml is a SimpleXMLElement. Temporarily set it as an XML string
+ // to prevent parent::hash() failing when try to create the hash.
+ return parent::hash($row->xml->asXML());
+ }
}
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/import/options.test b/sites/all/modules/contrib/migrate/migrate/tests/import/options.test
index 090bdfec..14b321b2 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/import/options.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/import/options.test
@@ -21,7 +21,7 @@ class MigrateImportOptionsTest extends DrupalWebTestCase {
parent::setUp('migrate_example');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testItemLimitOption() {
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/comment.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/comment.test
index d285f715..d3005d7b 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/comment.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/comment.test
@@ -21,7 +21,7 @@ class MigrateCommentUnitTest extends DrupalWebTestCase {
parent::setUp('taxonomy', 'image', 'comment', 'migrate', 'migrate_example');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testCommentImport() {
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/node.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/node.test
index d8c1f8f8..437e2ca4 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/node.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/node.test
@@ -21,7 +21,7 @@ class MigrateNodeUnitTest extends DrupalWebTestCase {
parent::setUp('list', 'number', 'taxonomy', 'image', 'migrate', 'migrate_example');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testNodeImport() {
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/table.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/table.test
index 02748f00..6314364e 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/table.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/table.test
@@ -21,7 +21,7 @@ class MigrateTableUnitTest extends DrupalWebTestCase {
parent::setUp('migrate', 'migrate_example');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testTableImport() {
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/term.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/term.test
index d50d38c5..7aba5f99 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/term.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/term.test
@@ -21,7 +21,7 @@ class MigrateTaxonomyUnitTest extends DrupalWebTestCase {
parent::setUp('taxonomy', 'migrate', 'migrate_example');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testTermImport() {
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/user.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/user.test
index e04a3795..33e62017 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/user.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/destinations/user.test
@@ -24,7 +24,7 @@ class MigrateUserUnitTest extends DrupalWebTestCase {
date_default_timezone_set('US/Mountain');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testUserImport() {
@@ -46,6 +46,14 @@ class MigrateUserUnitTest extends DrupalWebTestCase {
$roles[$row->name] = $row->rid;
}
$this->assertEqual(count($roles), 2, t('Both roles imported'));
+
+ // Make sure update does not fail (regression test of
+ // http://drupal.org/node/1872446)
+ $migration->prepareUpdate();
+ $migration->processImport();
+ $num_messages = $migration->getMap()->messageCount();
+ $this->assertEqual($num_messages, 0, t('No messages generated'));
+
$migration = Migration::getInstance('WineUser');
$result = $migration->processImport();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
@@ -106,9 +114,10 @@ class MigrateUserUnitTest extends DrupalWebTestCase {
1, t('Female gender migrated'));
$this->assert(!isset($users['fonzie']->field_migrate_example_gender[LANGUAGE_NONE][0]['value']),
t('Missing gender left unmigrated'));
+/* For some reason, this fails on d.o but not in local environments
$this->assert(is_object($users['fonzie']->picture) &&
- $users['fonzie']->picture->filename == 'association-individual.png',
- t('Picture migrated'));
+ $users['fonzie']->picture->filename == '200',
+ t('Picture migrated'));*/
$this->assertNotNull($users['fonzie']->roles[$roles['Taster']], t('Taster role'));
$this->assertNotNull($users['fonzie']->roles[$roles['Vintner']], t('Vintner role'));
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/oracle.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/oracle.test
index 57674a70..43f93a60 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/oracle.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/oracle.test
@@ -31,7 +31,7 @@ class MigrateOracleUnitTest extends DrupalWebTestCase {
}
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testOracleImport() {
diff --git a/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/xml.test b/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/xml.test
index 3dac554e..1b4eeff8 100644
--- a/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/xml.test
+++ b/sites/all/modules/contrib/migrate/migrate/tests/plugins/sources/xml.test
@@ -21,7 +21,7 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
parent::setUp('taxonomy', 'migrate', 'migrate_example');
// Make sure the migrations are registered.
- migrate_get_module_apis();
+ migrate_static_registration();
}
function testNodeImport() {
@@ -29,22 +29,51 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
$result = $migration->processImport();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
t('Region term import returned RESULT_COMPLETED'));
+
$migration = Migration::getInstance('WineFileCopy');
$result = $migration->processImport();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
t('File import returned RESULT_COMPLETED'));
+
$migration = Migration::getInstance('WineRole');
$result = $migration->processImport();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
t('Role import returned RESULT_COMPLETED'));
+
$migration = Migration::getInstance('WineUser');
$result = $migration->processImport();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
t('User import returned RESULT_COMPLETED'));
- $migration = Migration::getInstance('WineProducerXML');
- $result = $migration->processImport();
+
+ $migration1 = Migration::getInstance('WineProducerXML');
+ $result = $migration1->processImport();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
- t('Producer node import returned RESULT_COMPLETED'));
+ t('Producer node import 1 returned RESULT_COMPLETED'));
+
+ $migration2 = Migration::getInstance('WineProducerNamespaceXML');
+ $result = $migration2->processImport();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node import 2 returned RESULT_COMPLETED'));
+
+ $migration3 = Migration::getInstance('WineProducerMultiXML');
+ $result = $migration3->processImport();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node import 3 returned RESULT_COMPLETED'));
+
+ $migration4 = Migration::getInstance('WineProducerMultiNamespaceXML');
+ $result = $migration4->processImport();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node import 4 returned RESULT_COMPLETED'));
+
+ $migration5 = Migration::getInstance('WineProducerXMLPull');
+ $result = $migration5->processImport();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node import 5 returned RESULT_COMPLETED'));
+
+ $migration6 = Migration::getInstance('WineProducerNamespaceXMLPull');
+ $result = $migration6->processImport();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node import 6 returned RESULT_COMPLETED'));
// Gather producer nodes, and their corresponding input data
$rawnodes = node_load_multiple(FALSE, array('type' => 'migrate_example_producer'), TRUE);
@@ -54,7 +83,7 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
$producer_nodes[$node->title] = $node;
}
- $this->assertEqual(count($producer_nodes), 1,
+ $this->assertEqual(count($producer_nodes), 10,
t('Counts of producer nodes and input rows match'));
// Test each base node field
@@ -81,10 +110,32 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
$this->assertEqual($region[0]['tid'], $term->tid,
t('region properly migrated'));
- // Test rollback
- $result = $migration->processRollback();
+ // Rollback producer migrations
+ $result = $migration1->processRollback();
$this->assertEqual($result, Migration::RESULT_COMPLETED,
- t('Producer node rollback returned RESULT_COMPLETED'));
+ t('Producer node rollback 1 returned RESULT_COMPLETED'));
+
+ $result = $migration2->processRollback();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node rollback 2 returned RESULT_COMPLETED'));
+
+ $result = $migration3->processRollback();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node rollback 3 returned RESULT_COMPLETED'));
+
+ $result = $migration4->processRollback();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node rollback 4 returned RESULT_COMPLETED'));
+
+ $result = $migration5->processRollback();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node rollback 5 returned RESULT_COMPLETED'));
+
+ $result = $migration6->processRollback();
+ $this->assertEqual($result, Migration::RESULT_COMPLETED,
+ t('Producer node rollback 6 returned RESULT_COMPLETED'));
+
+ // Test rollback
$rawnodes = node_load_multiple(FALSE, array('type' => 'migrate_example_producer'), TRUE);
$this->assertEqual(count($rawnodes), 0, t('All nodes deleted'));
$count = db_select('migrate_map_wineproducerxml', 'map')
diff --git a/sites/all/modules/contrib/panels/panels/css/panels_dnd.css b/sites/all/modules/contrib/panels/panels/css/panels_dnd.css
index 530b359d..20fad13e 100644
--- a/sites/all/modules/contrib/panels/panels/css/panels_dnd.css
+++ b/sites/all/modules/contrib/panels/panels/css/panels_dnd.css
@@ -175,7 +175,7 @@ div.panels-set-title-hide .panel-pane-is-title {
margin: 0;
}
-.panel-portlet .buttons input {
+.panel-portlet .buttons input, .panel-portlet .buttons button {
margin: 0;
padding: 0;
display: inline;
@@ -369,22 +369,25 @@ a.close img {
text-align: left;
}
+.content-type-button a {
+ display: inline-block;
+ width: 100%;
+}
+
.content-type-button img {
border: 2px solid white;
- float: left;
}
.content-type-button img:hover {
border: 2px solid blue;
}
-.content-type-button div {
+.content-type-button div,
+.content-type-button span {
width: 85%;
+ position: relative;
top: -5px;
- left: 2px;
- float: left;
- padding-left: 3px;
- padding-top: 5px;
+ left: 3px;
}
#panels-preview .modal-throbber-wrapper {
diff --git a/sites/all/modules/contrib/panels/panels/i18n_panels/README.txt b/sites/all/modules/contrib/panels/panels/i18n_panels/README.txt
new file mode 100644
index 00000000..71c4b8f9
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/i18n_panels/README.txt
@@ -0,0 +1,93 @@
+
+This module provides by default the ability to translate panel display and
+panel pane titles.
+Further it introduced an extension to the ctools content_types plugin.
+You can now define translatable settings which will be registered in i18n.
+Out of the box the module extends the custom content content_type to allow
+translation of the content.
+
+Requirements:
+ Ctools 7.x-1.x-dev (Jan 28-2014 or newer)
+ Panels 7.x-3.x-dev (Jan 28-2014 or newer)
+
+Plugin definition extension:
+------------------------------
+
+This example shows how the content_type custom is extended:
+
+#### Default: ####
+/**
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
+ */
+$plugin = array(
+ 'title' => t('Custom content'),
+ 'no title override' => TRUE,
+ 'defaults' => array('admin_title' => '', 'title' => '', 'body' => '', 'format' => filter_fallback_format(), 'substitute' => TRUE),
+ 'js' => array('misc/autocomplete.js', 'misc/textarea.js', 'misc/collapse.js'),
+ // Make sure the edit form is only used for some subtypes.
+ 'edit form' => '',
+ 'add form' => '',
+ 'edit text' => t('Edit'),
+ 'all contexts' => TRUE,
+);
+
+#### Extended Configuration: ####
+/**
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
+ */
+$plugin = array(
+ 'title' => t('Custom content'),
+ 'no title override' => TRUE,
+ 'defaults' => array('admin_title' => '', 'title' => '', 'body' => '', 'format' => filter_fallback_format(), 'substitute' => TRUE),
+ 'js' => array('misc/autocomplete.js', 'misc/textarea.js', 'misc/collapse.js'),
+ // Make sure the edit form is only used for some subtypes.
+ 'edit form' => '',
+ 'add form' => '',
+ 'edit text' => t('Edit'),
+ 'all contexts' => TRUE,
+ 'i18n_settings' = array(
+ 'title',
+ 'body' => array('format' => 'plain_text'),
+ 'items|0|title'
+ ),
+);
+
+The new key "i18n_settings" defines an array with the settings that are
+translatable. The array contains the names of the settings, they have to be
+available in the "defaults" array of the content definition. If you need to
+define a format use the name of the setting as the array item key and as item
+another array with the detail configuration. E.g
+'i18n_settings' = array('body' => array('format' => 'plain_text'))
+
+If i18n_settings is a string it's used as callback. The expected return is an
+array equal to the one used in the fix configuration.
+You can even declare nested settings as translatable, to do so use '|' as
+delimiter.
+E.g. 'items|0|title' is evaluated as $settings['items'][0]['title']
+
+#### Callback: ####
+/**
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
+ */
+$plugin = array(
+ 'title' => t('Custom content'),
+ 'no title override' => TRUE,
+ 'defaults' => array('admin_title' => '', 'title' => '', 'body' => '', 'format' => filter_fallback_format(), 'substitute' => TRUE),
+ 'js' => array('misc/autocomplete.js', 'misc/textarea.js', 'misc/collapse.js'),
+ // Make sure the edit form is only used for some subtypes.
+ 'edit form' => '',
+ 'add form' => '',
+ 'edit text' => t('Edit'),
+ 'all contexts' => TRUE,
+ 'i18n_settings' => 'ctools_custom_content_type_i18n_settings',
+);
+
+function ctools_custom_content_type_i18n_settings($conf) {
+ return array(
+ 'title',
+ 'body' => array('format' => $conf['format']),
+ );
+}
diff --git a/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.i18n.inc b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.i18n.inc
new file mode 100644
index 00000000..71fdafb7
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.i18n.inc
@@ -0,0 +1,50 @@
+ t('Pane Configuration'),
+ 'key' => 'uuid',
+ 'string translation' => array(
+ 'textgroup' => 'panels',
+ 'type' => 'pane_configuration',
+ 'properties' => array(
+ 'title' => t('Pane Title'),
+ ),
+ ),
+ );
+ $info['display_configuration'] = array(
+ 'title' => t('Display Configuration'),
+ 'key' => 'uuid',
+ 'string translation' => array(
+ 'textgroup' => 'panels',
+ 'type' => 'display_configuration',
+ 'properties' => array(
+ 'title' => t('Display Title'),
+ ),
+ ),
+ );
+
+ return $info;
+}
+
+/**
+ * Implements hook_i18n_string_info().
+ */
+function i18n_panels_i18n_string_info() {
+ $groups['panels'] = array(
+ 'title' => t('Panels'),
+ 'description' => t('Translatable panels items: display and pane configuration items. E.g. Title.'),
+ // This group doesn't have strings with format.
+ 'format' => FALSE,
+ // This group can list all strings.
+ 'list' => FALSE,
+ );
+ return $groups;
+}
diff --git a/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.info b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.info
new file mode 100644
index 00000000..13838a1f
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.info
@@ -0,0 +1,15 @@
+name = Panels translation
+description = Supports translatable panels items.
+dependencies[] = i18n
+dependencies[] = panels
+dependencies[] = i18n_string
+dependencies[] = i18n_translation
+package = Multilingual - Internationalization
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-3.5"
+core = "7.x"
+project = "panels"
+datestamp = "1422472985"
+
diff --git a/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.install b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.install
new file mode 100644
index 00000000..ff7631b8
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.install
@@ -0,0 +1,27 @@
+ t('Panels uuid support.'),
+ 'severity' => REQUIREMENT_OK,
+ 'value' => t('Available'),
+ );
+ if (!db_field_exists('panels_pane', 'uuid')) {
+ $requirements['uuid']['severity'] = REQUIREMENT_ERROR;
+ $requirements['uuid']['value'] = t('Not found. Please apply the provided patches and run the update script.');
+ }
+ }
+ return $requirements;
+}
diff --git a/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.module b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.module
new file mode 100644
index 00000000..91524e3d
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/i18n_panels/i18n_panels.module
@@ -0,0 +1,442 @@
+type);
+ if (isset($content_type['i18n_settings'])) {
+ if (is_string($content_type['i18n_settings']) && function_exists($content_type['i18n_settings'])) {
+ $content_type['i18n_settings'] = $content_type['i18n_settings']($pane->configuration);
+ }
+ }
+ // Provide the override title string as translation for all panes that have
+ // this setting enabled.
+ if (isset($pane->configuration['override_title']) && $pane->configuration['override_title']) {
+ if (isset($content_type['i18n_settings']) && is_array($content_type['i18n_settings'])) {
+ $content_type['i18n_settings'][] = 'override_title_text';
+ }
+ else {
+ $content_type['i18n_settings'] = array('override_title_text');
+ }
+ }
+ return isset($content_type['i18n_settings']) ? $content_type['i18n_settings'] : FALSE;
+}
+
+/**
+ * Returns the translation object of the pane.
+ *
+ * @param stdClass $pane
+ * The pane to deal with.
+ *
+ * @return stdClass|FALSE
+ * Returns FALSE if no translation is necessary.
+ */
+function i18n_panels_get_i18n_translation_object($pane) {
+ $translation_object = array();
+
+ // Handle content type specific i18n settings.
+ if ($i18n_settings = i18n_panels_get_i18n_settings($pane)) {
+ // Register translatable settings.
+ foreach ($i18n_settings as $i18n_setting => $settings) {
+ if (!is_array($settings)) {
+ $i18n_setting = $settings;
+ $settings = array('format' => 'plain_text');
+ }
+ $translation_object[$i18n_setting] = NULL;
+ $key_exists = FALSE;
+ // Ensure a nested setting is "unpacked".
+ $config_value = drupal_array_get_nested_value($pane->configuration, explode('|', $i18n_setting), $key_exists);
+ // If we reached the end of the nested setting use the value as source.
+ if ($key_exists) {
+ $translation_object[$i18n_setting] = array(
+ 'string' => $config_value,
+ 'format' => $settings['format'],
+ );
+ $translation_object['panels_i18n_settings'][$i18n_setting] = $settings;
+ }
+ }
+ }
+
+ // Check if this pane has a custom title enabled.
+ if (!empty($pane->configuration['override_title'])) {
+ $translation_object['title']['string'] = $pane->configuration['override_title_text'];
+ }
+ if (!empty($translation_object)) {
+ return (object) $translation_object;
+ }
+ return FALSE;
+}
+
+/**
+ * Implements hook_panels_pane_insert().
+ *
+ * @param stdClass $pane
+ * The pane to deal with.
+ */
+function i18n_panels_panels_pane_insert($pane) {
+ i18n_panels_panels_pane_update($pane);
+}
+
+/**
+ * Implements hook_panels_pane_update().
+ *
+ * @param stdClass $pane
+ * The pane to deal with.
+ */
+function i18n_panels_panels_pane_update($pane) {
+ if ($translation_object = i18n_panels_get_i18n_translation_object($pane)) {
+ $translation_object->uuid = $pane->uuid;
+ $status = i18n_string_object_update('pane_configuration', $translation_object);
+ }
+}
+
+/**
+ * Implements hook_panels_pane_delete().
+ *
+ * @param array $pids
+ * Array with the panel ids to delete.
+ */
+function i18n_panels_panels_pane_delete($pids) {
+ if (!empty($pids)) {
+ // Fetch the uuids from the db.
+ $uuids = db_select('panels_pane')
+ ->fields('panels_pane', array('uuid'))
+ ->condition('pid', $pids)
+ ->execute()
+ ->fetchCol();
+ foreach ($uuids as $uuid) {
+ // Create dummy pane with uuid as property.
+ $pane = (object) array('uuid' => $uuid);
+ i18n_string_object_remove('pane_configuration', $pane);
+ }
+ }
+}
+
+/**
+ * Implements hook_panels_pane_prerender().
+ *
+ * @param stdClass $pane
+ * The pane to deal with.
+ */
+function i18n_panels_panels_pane_prerender($pane) {
+ // Check if this pane has translations.
+ if (isset($pane->uuid) && $translation_object = i18n_panels_get_i18n_translation_object($pane)) {
+ $translation_object->uuid = $pane->uuid;
+ // Send to translation.
+ $translation_object = i18n_string_object_translate('pane_configuration', $translation_object);
+ unset($translation_object->uuid, $translation_object->i18n_settings);
+ foreach ($translation_object as $i18n_setting => $translated_setting) {
+ if ($i18n_setting != 'panels_i18n_settings') {
+ if (is_array($translated_setting)) {
+ $translated_setting = $translated_setting['string'];
+ }
+ drupal_array_set_nested_value($pane->configuration, explode('|', $i18n_setting), $translated_setting);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_panels_display_save().
+ *
+ * @param panels_display $display
+ * The display to deal with.
+ */
+function i18n_panels_panels_display_save($display) {
+ $status = i18n_string_object_update('display_configuration', $display);
+}
+
+/**
+ * Implements hook_panels_display_delete().
+ *
+ * @param int $did
+ * Id of the display to delete.
+ */
+function i18n_panels_panels_delete_display($did) {
+ // Fetch uuid to delete the translations.
+ $uuid = db_select('panels_display')
+ ->fields('panels_display', array('uuid'))
+ ->condition('did', $did)
+ ->execute()
+ ->fetchColumn();
+ // Build a dummy display.
+ $display = (object) array('uuid' => $uuid);
+
+ // Check if this display was just saved in the db.
+ if (!_18n_panels_is_exported_panels_display($display)) {
+ // If the display was just saved in the db remove all translations.
+ i18n_string_object_remove('display_configuration', $display);
+ // Remove related pane translations too.
+ $pids = db_select('panels_pane')
+ ->fields('panels_pane', array('pid'))
+ ->condition('did', $did)
+ ->execute()
+ ->fetchCol();
+ i18n_panels_panels_pane_delete($pids);
+ }
+ else {
+ // If the display is exported leave the translated strings but give the user
+ // a hint how to clean up.
+ drupal_set_message(
+ t(
+ 'The reverted panels display(s) were exported, please run a string refresh to update the translatable strings.',
+ array('!link' => url('admin/config/regional/translate/i18n_string'))
+ ),
+ 'warning',
+ FALSE
+ );
+ }
+}
+
+/**
+ * Implements hook_panels_pre_render().
+ *
+ * This function must not rely on the passed $renderer parameter. The parameter
+ * could be empty because this function is reused in i18n_ctools_render_alter().
+ * @todo Check if a drupal_alter() in panels_display::get_title() is applicable.
+ *
+ * @see i18n_ctools_render_alter()
+ *
+ * @param panels_display $display
+ * The display to deal with.
+ * @param panels_renderer_standard $renderer
+ * The renderer to deal with.
+ */
+function i18n_panels_panels_pre_render(&$display, $renderer) {
+ // Avoid double translations.
+ if (!isset($display->i18n_panels_title_translated)) {
+ $translation = i18n_string_object_translate('display_configuration', $display);
+ if (is_array($translation->title)) {
+ $display->title = $translation->title['string'];
+ }
+ else {
+ $display->title = $translation->title;
+ }
+ $display->i18n_panels_title_translated = TRUE;
+ }
+}
+
+/**
+ * Implements hook_ctools_render_alter().
+ *
+ * Under some circumstances the title of the panel page is set before
+ * hook_panels_pre_render() is fired. Such cases can be handled with this hook.
+ * @todo Check if a drupal_alter() in panels_display::get_title() is applicable.
+ */
+function i18n_ctools_render_alter(&$info, $page, $context) {
+ // @todo Find a better way to detect a panels page.
+ if ($page === TRUE && !empty($info['content']['#display']) && $info['content']['#display'] instanceof panels_display) {
+ i18n_panels_panels_pre_render($info['content']['#display'], NULL);
+ // Set the info title. This is used to set the page title.
+ $info['title'] = $info['content']['#display']->get_title();
+ }
+}
+
+
+/**
+ * Implements hook_ctools_plugin_post_alter().
+ *
+ * Register some translatable configuration settings for plugins.
+ *
+ */
+function i18n_panels_ctools_plugin_post_alter(&$plugin, $plugin_type_info) {
+ if ($plugin_type_info['type'] == 'content_types') {
+ // Modify custom content.
+ if ($plugin['name'] == 'custom') {
+ // Register callback to get the translatable settings.
+ $plugin['i18n_settings'] = 'ctools_custom_content_type_i18n_settings';
+ }
+ }
+}
+
+/**
+ * Callback to provide the translatable settings appropriate to the config.
+ *
+ * @param array $conf
+ * Content type configuration.
+ *
+ * @return array
+ * i18n_settings configuration.
+ */
+function ctools_custom_content_type_i18n_settings($conf) {
+ return array(
+ 'title',
+ 'body' => array('format' => $conf['format']),
+ );
+}
+
+/**
+ * Implements hook_i18n_string_list_TEXTGROUP_alter().
+ *
+ * Necessary to support the dynamic translatable settings defined by ctools
+ * content types.
+ */
+function i18n_panels_i18n_string_list_panels_alter(&$strings, $type = NULL, $object = NULL) {
+ if (isset($object->panels_i18n_settings)) {
+ foreach ($object->panels_i18n_settings as $i18n_setting => $settings) {
+ if (isset($object->{$i18n_setting})) {
+ $strings['panels'][$type][$object->uuid][$i18n_setting] = $object->{$i18n_setting};
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_i18n_string_list().
+ *
+ * @todo Figure out a generic solution to fetch exported displays.
+ */
+function i18n_panels_i18n_string_list($group) {
+ $strings = array();
+ if ($group == 'panels') {
+
+ // Fetch all available displays.
+ $displays = _18n_panels_fetch_all_panel_displays();
+
+ foreach ($displays as $display) {
+ if (empty($display->uuid)) {
+ drupal_set_message(t('The display %display has no uuid, please resave or re-export it.', array('%display' => $display->did)), 'warning');
+ continue;
+ }
+ // Avoid duplicated runs _18n_panels_fetch_all_panel_displays() probably
+ // returns the same display twice, one for the db based and one for the
+ // exported one.
+ if (isset($strings['panels']['display_configuration'][$display->uuid])) {
+ continue;
+ }
+ $strings['panels']['display_configuration'][$display->uuid]['title']['string'] = $display->title;
+ foreach ($display->content as $pane) {
+ if (empty($pane->uuid)) {
+ // Fetch exported uuid and validate it.
+ $uuid = str_replace('new-', '', $pane->pid);
+ if (!panels_uuid_is_valid($uuid)) {
+ drupal_set_message(t('The pane %pane has no uuid, please resave or re-export it.', array('%pane' => $pane->pid)), 'warning');
+ continue;
+ }
+ $pane->uuid = $uuid;
+ }
+ if ($translation_object = i18n_panels_get_i18n_translation_object($pane)) {
+ // Split up all strings and add them to the list.
+ $pane_strings = (array) $translation_object;
+ unset($pane_strings['panels_i18n_settings']);
+ foreach ($pane_strings as $key => $pane_string) {
+ $strings['panels']['pane_configuration'][$pane->uuid][$key] = $pane_string;
+ }
+ }
+ }
+ }
+ }
+ return $strings;
+}
+
+/**
+ * Checks if the give display is exported or only stored in the db.
+ *
+ * @return boolean
+ * TRUE if the display is available from code.
+ */
+function _18n_panels_is_exported_panels_display($display) {
+ if (isset($display->uuid)) {
+ $displays = _18n_panels_fetch_all_panel_displays();
+ return isset($displays['exported-' . $display->uuid]);
+ }
+ return FALSE;
+}
+
+/**
+ * Returns a list of really all available panel displays.
+ *
+ * The list is statically cached. Use the parameter $reset to refresh the list
+ * during the same request.
+ * Probably returns the same display twice - once with the db based and once
+ * the exported one.
+ *
+ * @todo I bet there are better ways to solve this mess.
+ *
+ * @param boolean $reset
+ * Reset the static cache.
+ *
+ * @return array
+ * List of all panel displays.
+ */
+function _18n_panels_fetch_all_panel_displays($reset = FALSE) {
+ $displays = &drupal_static(__FUNCTION__, array());
+ if (!empty($displays) && !$reset) {
+ return $displays;
+ }
+
+ // Fetch db based displays.
+ $dids = db_select('panels_display')->fields('panels_display', array('did'))->execute()->fetchCol();
+ $displays = panels_load_displays($dids);
+
+ // Fetch exported displays.
+ ctools_include('export');
+ foreach (ctools_export_crud_load_all('panels_display') as $panels_display) {
+ if (!empty($panels_display->uuid)) {
+ $displays['exported-' . $panels_display->uuid] = $panels_display;
+ }
+ }
+
+ // Fetch mini panels.
+ $mini_panels = ctools_export_crud_load_all('panels_mini');
+ foreach ($mini_panels as $pane) {
+ if (!empty($pane->display->uuid)) {
+ $displays['exported-' . $pane->display->uuid] = $pane->display;
+ }
+ }
+
+ // Fetch in page manager embedded displays.
+ if (module_exists('page_manager')) {
+ module_load_include('inc', 'page_manager', 'page_manager.admin');
+ $tasks = page_manager_get_tasks_by_type('page');
+ $pages = array('operations' => array(), 'tasks' => array());
+ page_manager_get_pages($tasks, $pages);
+
+ foreach ($pages['tasks'] as $task) {
+ $page = page_manager_cache_load($task);
+ $task_info = page_manager_get_task_subtasks($page->task);
+ foreach ($page->handler_info as $id => $info) {
+ $page_manager_handler = $page->handlers[$id];
+ if ($page_manager_handler->handler == 'panel_context') {
+
+ // @todo Is there really no better way to check this?
+ $is_exported = ($page_manager_handler->export_type == (EXPORT_IN_CODE | EXPORT_IN_DATABASE) || (isset($page->subtask['storage']) && $page->subtask['storage'] == t('Overridden')));
+
+ if (!empty($page_manager_handler->conf['display'])) {
+ $panels_display = $page_manager_handler->conf['display'];
+ $displays['exported-' . $panels_display->uuid] = $panels_display;
+ }
+ elseif ($is_exported && isset($page_manager_handler->conf['did'])) {
+ $panels_display = panels_load_display($page_manager_handler->conf['did']);
+ if (isset($panels_display->uuid)) {
+ $displays['exported-' . $panels_display->uuid] = $panels_display;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Fetch panelizer displays.
+ if (module_exists('panelizer')) {
+ // Fetch all default handlers.
+ $panelizer_defaults = ctools_export_crud_load_all('panelizer_defaults');
+ foreach ($panelizer_defaults as $panelizer_default) {
+ $displays['exported-' . $panelizer_default->display->uuid] = $panelizer_default->display;
+ }
+ }
+ return $displays;
+}
diff --git a/sites/all/modules/contrib/panels/panels/includes/add-content.inc b/sites/all/modules/contrib/panels/panels/includes/add-content.inc
index 0a52ff92..dc0be48f 100644
--- a/sites/all/modules/contrib/panels/panels/includes/add-content.inc
+++ b/sites/all/modules/contrib/panels/panels/includes/add-content.inc
@@ -77,7 +77,10 @@ function template_preprocess_panels_add_content_link(&$vars) {
$vars['description'] = isset($vars['content_type']['description']) ? $vars['content_type']['description'] : $vars['title'];
$vars['icon'] = ctools_content_admin_icon($vars['content_type']);
$vars['url'] = $vars['renderer']->get_url('add-pane', $vars['region'], $vars['content_type']['type_name'], $vars['content_type']['subtype_name']);
-
- $vars['image_button'] = ctools_ajax_image_button($vars['icon'], $vars['url'], $vars['description'], 'panels-modal-add-config');
- $vars['text_button'] = ctools_ajax_text_button($vars['title'], $vars['url'], $vars['description'], 'panels-modal-add-config');
+ $subtype_class = 'add-content-link-' . str_replace('_', '-', $vars['content_type']['subtype_name']);
+ $vars['image_button'] = ctools_ajax_image_button($vars['icon'], $vars['url'], $vars['description'], $subtype_class . '-image-button panels-modal-add-config');
+ $vars['text_button'] = ctools_ajax_text_button($vars['title'], $vars['url'], $vars['description'], $subtype_class . '-text-button panels-modal-add-config');
+ if (function_exists('ctools_ajax_icon_text_button')) {
+ $vars['icon_text_button'] = ctools_ajax_icon_text_button($vars['title'], $vars['icon'], $vars['url'], $vars['description'], $subtype_class . '-icon-text-button panels-modal-add-config');
+ }
}
diff --git a/sites/all/modules/contrib/panels/panels/includes/callbacks.inc b/sites/all/modules/contrib/panels/panels/includes/callbacks.inc
index 5188394e..255a3cdd 100644
--- a/sites/all/modules/contrib/panels/panels/includes/callbacks.inc
+++ b/sites/all/modules/contrib/panels/panels/includes/callbacks.inc
@@ -172,6 +172,22 @@ function panels_admin_settings_page() {
}
}
+ ctools_include('plugins', 'panels');
+ $pipelines = panels_get_renderer_pipelines();
+ $options = array();
+ foreach ($pipelines as $key => $value) {
+ $options[$key] = $value->admin_title;
+ }
+ if (count($options) > 1) {
+ $form['panels_renderer_default'] = array(
+ '#type' => 'select',
+ '#title' => t('Default renderer'),
+ '#options' => $options,
+ '#default_value' => variable_get('panels_renderer_default', 'standard'),
+ '#description' => t('The default renderer for new panel pages.'),
+ );
+ }
+
if (empty($form)) {
return array('#value' => t('There are currently no settings to change, but additional plugins or modules may provide them in the future.'));
}
diff --git a/sites/all/modules/contrib/panels/panels/includes/common.inc b/sites/all/modules/contrib/panels/panels/includes/common.inc
index 39f14b53..120f0a78 100644
--- a/sites/all/modules/contrib/panels/panels/includes/common.inc
+++ b/sites/all/modules/contrib/panels/panels/includes/common.inc
@@ -477,6 +477,7 @@ function panels_common_get_allowed_layout_object($module_name) {
* Get the allowed layouts for the given module.
*/
function panels_common_get_allowed_layouts($module_name) {
+ ctools_include('plugins', 'panels');
$available_layouts = panels_get_layouts();
if (empty($module_name)) {
return $available_layouts;
diff --git a/sites/all/modules/contrib/panels/panels/includes/display-edit.inc b/sites/all/modules/contrib/panels/panels/includes/display-edit.inc
index dc6d1b91..4e282924 100644
--- a/sites/all/modules/contrib/panels/panels/includes/display-edit.inc
+++ b/sites/all/modules/contrib/panels/panels/includes/display-edit.inc
@@ -240,7 +240,7 @@ function panels_edit_display_settings_form($form, &$form_state) {
'#type' => 'textfield',
'#default_value' => $display->title,
'#title' => t('Title'),
- '#description' => t('The title of this panel. If left blank, a default title may be used. Set to No Title if you want the title to actually be blank.'),
+ '#description' => t('The title of this panel. If left blank, a default title may be used. If you want the title actually to be blank, change the "Title type" dropdown from "Manually Set" to "No Title".'),
'#process' => array('ctools_dependent_process'),
'#dependency' => array('edit-display-title-hide-title' => array(PANELS_TITLE_FIXED)),
'#maxlength' => 255,
diff --git a/sites/all/modules/contrib/panels/panels/includes/display-layout.inc b/sites/all/modules/contrib/panels/panels/includes/display-layout.inc
index ad051b45..b4687e2a 100644
--- a/sites/all/modules/contrib/panels/panels/includes/display-layout.inc
+++ b/sites/all/modules/contrib/panels/panels/includes/display-layout.inc
@@ -94,6 +94,7 @@ function panels_choose_layout($form, &$form_state) {
}
}
+ ctools_add_js('panels-base', 'panels');
ctools_add_js('layout', 'panels');
$form['categories'] = array(
diff --git a/sites/all/modules/contrib/panels/panels/includes/plugins.inc b/sites/all/modules/contrib/panels/panels/includes/plugins.inc
index 9077d382..b4706681 100644
--- a/sites/all/modules/contrib/panels/panels/includes/plugins.inc
+++ b/sites/all/modules/contrib/panels/panels/includes/plugins.inc
@@ -173,15 +173,36 @@ class panels_cache_object {
$start = $this->js;
$this->js = array();
+ // Use the advanced mapping function from Drupal >= 7.23 if available.
+ $array_mapping_func = function_exists('drupal_array_diff_assoc_recursive') ? 'drupal_array_diff_assoc_recursive' : 'array_diff_assoc';
+
// If there are any differences between the old and the new javascript then
// store them to be added later.
- if ($diff = array_diff_assoc($js, $start)) {
- $this->js = $diff;
- }
-
- // Special case the settings key and get the difference of the data.
- if ($settings_diff = array_diff_assoc($js['settings']['data'], $start['settings']['data'])) {
- $this->js['settings'] = $settings_diff;
+ if ($diff = $array_mapping_func($js, $start)) {
+ // Iterate over the diff to ensure we keep the keys on merge and don't add
+ // unnecessary items.
+ foreach ($diff as $key => $diff_data) {
+ // Special case the settings key and get the difference of the data.
+ if ($key === 'settings') {
+ // Iterate over the diff to ensure we keep the keys on merge and don't
+ // add unnecessary items.
+ if (isset($diff[$key]['data'])) {
+ foreach ($diff[$key]['data'] as $settings_key => $settings_data) {
+ // Merge the changes with the base to get a complete settings
+ // array.
+ $this->js[$key]['data'][] = drupal_array_merge_deep($settings_data, $diff[$key]['data'][$settings_key]);
+ }
+ }
+ }
+ else {
+ $this->js[$key] = $diff_data;
+ // Check if the key was present already and if so merge the changes
+ // with the original data to get the full settings array.
+ if (isset($start[$key])) {
+ $this->js[$key] = drupal_array_merge_deep($start[$key], $this->js[$key]);
+ }
+ }
+ }
}
// And for tokens:
@@ -213,7 +234,7 @@ class panels_cache_object {
drupal_add_js($args['data'], $args);
}
else {
- foreach ($args as $setting) {
+ foreach ($args['data'] as $setting) {
drupal_add_js($setting, 'setting');
}
}
diff --git a/sites/all/modules/contrib/panels/panels/js/display_editor.js b/sites/all/modules/contrib/panels/panels/js/display_editor.js
index d873c9d3..53274f68 100644
--- a/sites/all/modules/contrib/panels/panels/js/display_editor.js
+++ b/sites/all/modules/contrib/panels/panels/js/display_editor.js
@@ -16,7 +16,7 @@ Drupal.Panels.bindClickDelete = function(context) {
$('a.pane-delete:not(.pane-delete-processed)', context)
.addClass('pane-delete-processed')
.click(function() {
- if (confirm('Remove this pane?')) {
+ if (confirm(Drupal.t('Remove this pane?'))) {
var id = '#' + $(this).attr('id').replace('pane-delete-', '');
$(id).remove();
Drupal.Panels.Draggable.savePositions();
@@ -80,7 +80,7 @@ Drupal.Panels.Draggable = {
regionId: 'panel-region-',
// What to add to the front of a the id to get the form id for a panel
- formId: 'input#edit-',
+ formId: '#edit-',
maxWidth: 250,
@@ -492,8 +492,8 @@ Drupal.behaviors.PanelsDisplayEditor = {
Drupal.Panels.Draggable.savePositions();
// Bind buttons.
- $('input#panels-hide-all', context).click(Drupal.Panels.clickHideAll);
- $('input#panels-show-all', context).click(Drupal.Panels.clickShowAll);
+ $('#panels-hide-all', context).click(Drupal.Panels.clickHideAll);
+ $('#panels-show-all', context).click(Drupal.Panels.clickShowAll);
Drupal.Panels.bindClickDelete(context);
@@ -513,6 +513,11 @@ Drupal.behaviors.PanelsDisplayEditor = {
$('#panels-preview').html(html);
});
+ // Bind modal detach behaviors to cancel current form.
+ $(document).bind('CToolsDetachBehaviors', function(event, context) {
+ $('#edit-cancel-style', context).trigger('click');
+ });
+
var setTitleClass = function () {
if ($('#edit-display-title-hide-title').val() == 2) {
$('#panels-dnd-main').removeClass('panels-set-title-hide');
diff --git a/sites/all/modules/contrib/panels/panels/js/panels.js b/sites/all/modules/contrib/panels/panels/js/panels.js
deleted file mode 100644
index 3bc5807a..00000000
--- a/sites/all/modules/contrib/panels/panels/js/panels.js
+++ /dev/null
@@ -1,28 +0,0 @@
-
-(function ($) {
- Drupal.Panels = Drupal.Panels || {};
-
- Drupal.Panels.autoAttach = function() {
- if ($.browser.msie) {
- // If IE, attach a hover event so we can see our admin links.
- $("div.panel-pane").hover(
- function() {
- $('div.panel-hide', this).addClass("panel-hide-hover"); return true;
- },
- function() {
- $('div.panel-hide', this).removeClass("panel-hide-hover"); return true;
- }
- );
- $("div.admin-links").hover(
- function() {
- $(this).addClass("admin-links-hover"); return true;
- },
- function(){
- $(this).removeClass("admin-links-hover"); return true;
- }
- );
- }
- };
-
- $(Drupal.Panels.autoAttach);
-})(jQuery);
diff --git a/sites/all/modules/contrib/panels/panels/panels.api.php b/sites/all/modules/contrib/panels/panels/panels.api.php
new file mode 100644
index 00000000..1aa1c6ad
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/panels.api.php
@@ -0,0 +1,264 @@
+mini_panels_display_cache = $cache;
+ $handler->edit_cache_set_key($item, $argument);
+}
+
+/**
+ * Allow modules to provide their own caching mechanism for the display editor.
+ *
+ * @param string $argument
+ * The second half of the cache key. Full key module:TASK_NAME:HANDLER_NAME
+ * passed part: TASK_NAME:HANDLER_NAME
+ *
+ * @return stdClass|NULL
+ * The cached display or NULL if the cache wasn't hit.
+ */
+function hook_panels_cache_get($argument) {
+ ctools_include('common', 'panels');
+ list($handler, $item) = _panels_mini_panels_cache_get($argument);
+ if (isset($item->mini_panels_display_cache)) {
+ return $item->mini_panels_display_cache;
+ }
+
+ $cache = new stdClass();
+ $cache->display = $item->display;
+ $cache->display->context = ctools_context_load_contexts($item);
+ $cache->display->cache_key = 'panels_mini:' . $argument;
+ $cache->content_types = panels_common_get_allowed_types('panels_mini', $cache->display->context);
+ $cache->display_title = TRUE;
+
+ // @TODO support locking.
+ $cache->locked = FALSE;
+
+ return $cache;
+}
+
+/**
+ * Allow modules to provide their own caching mechanism for the display editor.
+ *
+ * @param string $argument
+ * The second half of the cache key. Full key module:TASK_NAME:HANDLER_NAME
+ * passed part: TASK_NAME:HANDLER_NAME
+ * @param stdClass $cache
+ * The display to cache.
+ *
+ * @return stdClass
+ * The cached display.
+ */
+function hook_panels_cache_save($argument, $cache) {
+ list($handler, $item) = _panels_mini_panels_cache_get($argument);
+ $item->display = $cache->display;
+ panels_mini_save($item);
+
+ $handler->edit_cache_clear($item);
+
+ return $item;
+}
+
+/**
+ * Allow modules to provide their own caching mechanism for the display editor.
+ *
+ * @param string $argument
+ * The second half of the cache key. Full key module:TASK_NAME:HANDLER_NAME
+ * passed part: TASK_NAME:HANDLER_NAME
+ * @param stdClass $cache
+ * The cached display.
+ */
+function hook_panels_cache_clear($argument, $cache) {
+ list($handler, $item) = _panels_mini_panels_cache_get($argument);
+ $handler->edit_cache_clear($item);
+}
+
+/**
+ * Allow modules to adjust the rendering array of the panels dashboard.
+ *
+ * @param array $vars
+ * The output variables.
+ */
+function hook_panels_dashboard_blocks(&$vars) {
+ $vars['links']['panels_node'] = array(
+ 'title' => l(t('Panel node'), 'node/add/panel'),
+ 'description' => t('Panel nodes are node content and appear in your searches, but are more limited than panel pages.'),
+ 'weight' => -1,
+ );
+}
+
+/**
+ * Allow to alter the pane content to render.
+ *
+ * This happens after the keyword substitution.
+ *
+ * @param stdClass $content
+ * The content block to render.
+ * @param stdClass $pane
+ * The pane object.
+ * @param array $args
+ * The display arguments.
+ * @param array $contexts
+ * Array with the used contexts.
+ */
+function hook_panels_pane_content_alter($content, $pane, $args, $contexts) {
+ // Don't display titles.
+ unset($content->title);
+}
+
+/**
+ * Allow modules to provide a mechanism to break locks.
+ *
+ * @param string $argument
+ * The second half of the cache key. Full key module:TASK_NAME:HANDLER_NAME
+ * passed part: TASK_NAME:HANDLER_NAME
+ * @param stdClass $cache
+ * The cached display.
+ */
+function hook_panels_edit_cache_break_lock($argument, $cache) {
+ $cache->locked = FALSE;
+}
+
+/**
+ * Fired before a panels display is rendered.
+ *
+ * Last chance to modify the panels display or add output before the keyword
+ * substitution runs and the panels display is rendered.
+ *
+ * @param panels_display $panels_display
+ * The panels display that will be rendered.
+ * @param stdClass $renderer
+ * The renderer object that will be used to render.
+ *
+ * @return string
+ * Additional output to add before the panels display.
+ */
+function hook_panels_pre_render($panels_display, $renderer) {
+ $translation = i18n_string_object_translate('panels_display_configuration', $panels_display);
+ $panels_display->title = $translation->title;
+}
+
+/**
+ * Fired after a panels display is rendered.
+ *
+ * Allow to add additional output after the output of the panels display.
+ *
+ * @param panels_display $panels_display
+ * The rendered panels display.
+ * @param stdClass $renderer
+ * The used renderer object.
+ *
+ * @return string
+ * Additional output to add after the panels display.
+ */
+function hook_panels_post_render($panels_display, $renderer) {
+ return t('Output proudly sponsored by panels.');
+}
+
+/**
+ * Fired before a new pane is inserted in the storage.
+ *
+ * @param stdClass $pane
+ * Pane that will be rendered.
+ */
+function hook_panels_pane_insert($pane) {
+ // Check if this pane has a custom title enabled.
+ if (!empty($pane->configuration['override_title'])) {
+ $translation_object = (object) array(
+ 'pid' => $pane->pid,
+ 'title' => $pane->configuration['override_title_text'],
+ );
+ $status = i18n_string_object_update('panels_pane_configuration', $translation_object);
+ }
+}
+
+/**
+ * Fired before a changed pane is updated in the storage.
+ *
+ * @param stdClass $pane
+ * Pane that will be rendered.
+ */
+function hook_panels_pane_update($pane) {
+ // Check if this pane has a custom title enabled.
+ if (!empty($pane->configuration['override_title'])) {
+ $translation_object = (object) array(
+ 'pid' => $pane->pid,
+ 'title' => $pane->configuration['override_title_text'],
+ );
+ $status = i18n_string_object_update('panels_pane_configuration', $translation_object);
+ }
+}
+
+/**
+ * Fired before a panel is rendered.
+ *
+ * Last chance to modify the pane before the keyword substitution runs and the
+ * pane is rendered.
+ *
+ * @param stdClass $pane
+ * Pane that will be rendered.
+ */
+function hook_panels_pane_prerender($pane) {
+ // Check if this pane has a custom title enabled.
+ if (!empty($pane->configuration['override_title'])) {
+ $translation_object = (object) array(
+ 'pid' => $pane->pid,
+ 'title' => $pane->configuration['override_title_text'],
+ );
+ $translation_object = i18n_string_object_translate('panels_pane_configuration', $translation_object);
+ $pane->configuration['override_title_text'] = $translation_object->title;
+ }
+}
+
+/**
+ * Fired before panes are deleted.
+ *
+ * @param array $pids
+ * Array with the panel id's to delete.
+ */
+function hook_panels_pane_delete($pids) {
+ foreach ($pids as $pid) {
+ // Create dummy pane with pid as property.
+ $pane = (object) array('pid' => $pid);
+ i18n_string_object_remove('panels_pane_configuration', $pane);
+ }
+}
+
+/**
+ * Fired after a display is saved.
+ *
+ * @param panels_display $display
+ * The display to save.
+ */
+function hook_panels_display_save($display) {
+ i18n_string_object_update('display_configuration', $display);
+}
+
+/**
+ * Fired before a display is deleted.
+ *
+ * @param integer $did
+ * Id of the display to delete.
+ */
+function hook_panels_delete_display($did) {
+ $uuid = db_select('panels_display')
+ ->fields('panels_display', array('uuid'))
+ ->condition('did', $did)
+ ->execute()
+ ->fetchColumn();
+ $display = (object) array('uuid' => $uuid);
+ i18n_string_object_remove('display_configuration', $display);
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels.info b/sites/all/modules/contrib/panels/panels/panels.info
index e1983056..5eb4db15 100644
--- a/sites/all/modules/contrib/panels/panels/panels.info
+++ b/sites/all/modules/contrib/panels/panels/panels.info
@@ -2,17 +2,18 @@ name = Panels
description = Core Panels display functions; provides no external UI, at least one other Panels module should be enabled.
core = 7.x
package = "Panels"
+version = PANELS_VERSION
configure = admin/structure/panels
-dependencies[] = ctools
+dependencies[] = ctools (>1.5)
files[] = panels.module
files[] = includes/common.inc
files[] = includes/legacy.inc
files[] = includes/plugins.inc
files[] = plugins/views/panels_views_plugin_row_fields.inc
-; Information added by drupal.org packaging script on 2012-08-18
-version = "7.x-3.3"
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-3.5"
core = "7.x"
project = "panels"
-datestamp = "1345319572"
+datestamp = "1422472985"
diff --git a/sites/all/modules/contrib/panels/panels/panels.install b/sites/all/modules/contrib/panels/panels/panels.install
index 6e83d442..1d82544b 100644
--- a/sites/all/modules/contrib/panels/panels/panels.install
+++ b/sites/all/modules/contrib/panels/panels/panels.install
@@ -17,7 +17,7 @@ function panels_requirements_install() {
// Assume that if the user is running an installation profile that both
// Panels and CTools are the same release.
if (!(defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install')) {
- // apparently the install process doesn't include .module files,
+ // Apparently the install process doesn't include .module files,
// so we need to force the issue in order for our versioning
// check to work.
if (!defined('PANELS_REQUIRED_CTOOLS_API')) {
@@ -30,24 +30,52 @@ function panels_requirements_install() {
include_once drupal_get_path('module', 'ctools') . '/ctools.module';
}
if (!module_invoke('ctools', 'api_version', PANELS_REQUIRED_CTOOLS_API)) {
- $requirements['panels_ctools'] = array(
- 'title' => $t('CTools API Version'),
- 'value' => CTOOLS_API_VERSION,
- 'severity' => REQUIREMENT_ERROR,
- 'description' => t('The CTools API version is too old for Panels. Panels needs at least %version.', array('%version' => PANELS_REQUIRED_CTOOLS_API))
- );
+ $requirements['panels_ctools'] = array(
+ 'title' => $t('CTools API Version'),
+ 'value' => CTOOLS_API_VERSION,
+ 'severity' => REQUIREMENT_ERROR,
+ 'description' => t('The CTools API version is too old for Panels. Panels needs at least %version.', array('%version' => PANELS_REQUIRED_CTOOLS_API)),
+ );
}
}
return $requirements;
}
/**
- * Implementation of hook_schema().
+ * Implements of hook_schema().
*/
function panels_schema() {
- // This should always point to our 'current' schema. This makes it relatively easy
- // to keep a record of schema as we make changes to it.
- return panels_schema_4();
+ // This should always point to our 'current' schema. This makes it relatively
+ // easy to keep a record of schema as we make changes to it.
+ return panels_schema_6();
+}
+
+function panels_schema_6() {
+ $schema = panels_schema_5();
+
+ $schema['cache_panels'] = drupal_get_schema_unprocessed('system', 'cache');
+
+ return $schema;
+}
+
+function panels_schema_5() {
+ $schema = panels_schema_4();
+
+ $schema['panels_display']['fields']['uuid'] = array(
+ 'type' => 'char',
+ 'length' => '36',
+ );
+ $schema['panels_display']['export']['key'] = 'uuid';
+ $schema['panels_display']['export']['key name'] = 'UUID';
+
+ $schema['panels_pane']['fields']['uuid'] = array(
+ 'type' => 'char',
+ 'length' => '36',
+ );
+ $schema['panels_pane']['export']['key'] = 'uuid';
+ $schema['panels_pane']['export']['key name'] = 'UUID';
+
+ return $schema;
}
function panels_schema_4() {
@@ -375,3 +403,92 @@ function panels_update_7301() {
return t('panels_pane.lock field already existed, update skipped.');
}
+
+/**
+ * Adding universally unique identifiers to panels.
+ */
+function panels_update_7302() {
+ if (!module_load_include('inc', 'ctools', 'includes/uuid')) {
+ throw new DrupalUpdateException(t('Ctools UUID support not detected. You must update to a more recent version of the ctools module.'));
+ }
+ // Load the schema.
+ $schema = panels_schema_5();
+ $msg = array();
+
+ // Add the uuid column to the pane table.
+ $table = 'panels_pane';
+ $field = 'uuid';
+ // Due to a previous failure, the column may already exist:
+ if (!db_field_exists($table, $field)) {
+ $spec = $schema[$table]['fields'][$field];
+ db_add_field($table, $field, $spec);
+ $msg[] = t('Added panels_pane.uuid column.');
+ }
+
+ // Add the uuid column to the display table.
+ $table = 'panels_display';
+ $field = 'uuid';
+ // Due to a previous failure, the column may already exist:
+ if (!db_field_exists($table, $field)) {
+ $spec = $schema[$table]['fields'][$field];
+ db_add_field($table, $field, $spec);
+ $msg[] = t('Added panels_display.uuid column.');
+ }
+
+ if (empty($msg)) {
+ $msg[] = t('UUID column already present in the panels_display & panels_pane tables.');
+ }
+
+ // Update all DB-based panes & displays to ensure that they all contain a UUID.
+ $display_dids = db_select('panels_display')
+ ->fields('panels_display', array('did'))
+ ->condition(db_or()
+ ->condition('uuid', '')
+ ->isNull('uuid')
+ )
+ ->execute()
+ ->fetchCol();
+
+ // Check the panes as well, for paranoia.
+ $pane_dids = db_select('panels_pane')
+ ->distinct()
+ ->fields('panels_pane', array('did'))
+ ->condition(db_or()
+ ->condition('uuid', '')
+ ->isNull('uuid')
+ )
+ ->execute()
+ ->fetchCol();
+
+ $dids = array_unique(array_merge($display_dids, $pane_dids));
+
+ // If the Panels module is disabled we don't have access to
+ // panels_load_displays().
+ if (!function_exists('panels_load_displays')) {
+ module_load_include('module', 'panels');
+ }
+ if ($displays = panels_load_displays($dids)) {
+ foreach ($displays as $display) {
+ // A display save also triggers pane saves.
+ panels_save_display($display);
+ }
+ $msg[] = t('Generated UUIDs for database-based panel displays and panes.');
+ }
+ else {
+ $msg[] = t('No database-based panel displays or panes for which to generate UUIDs.');
+ }
+
+ return implode("\n", $msg);
+}
+
+/**
+ * Add a custom cache table for Panels.
+ */
+function panels_update_7303() {
+ $schema = panels_schema_6();
+
+ $table_name = 'cache_panels';
+ if (!db_table_exists($table_name)) {
+ db_create_table($table_name, $schema[$table_name]);
+ }
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels.module b/sites/all/modules/contrib/panels/panels/panels.module
index b312033d..1136ddb0 100644
--- a/sites/all/modules/contrib/panels/panels/panels.module
+++ b/sites/all/modules/contrib/panels/panels/panels.module
@@ -6,7 +6,22 @@
* Core functionality for the Panels engine.
*/
-define('PANELS_REQUIRED_CTOOLS_API', '2.0-alpha');
+define('PANELS_REQUIRED_CTOOLS_API', '2.0.8');
+
+/**
+ * The current working panels version.
+ *
+ * In a release, it should be 7.x-3.x, which should match what drush make will
+ * create. In a dev format, it should be 7.x-3.(x+1)-dev, which will allow
+ * modules depending on new features in panels to depend on panels > 7.x-3.x.
+ *
+ * To define a specific version of Panels as a dependency for another module,
+ * simply include a dependency line in that module's info file, e.g.:
+ * ; Requires Panels v7.x-3.4 or newer.
+ * dependencies[] = panels (>=3.4)
+ */
+define('PANELS_VERSION', '7.x-3.5');
+
define('PANELS_TITLE_FIXED', 0); // Hide title use to be true/false. So false remains old behavior.
define('PANELS_TITLE_NONE', 1); // And true meant no title.
@@ -43,7 +58,7 @@ function panels_theme() {
'variables' => array('id' => NULL, 'image' => NULL, 'title' => NULL),
);
$theme['panels_pane'] = array(
- 'variables' => array('output' => array(), 'pane' => array(), 'display' => array()),
+ 'variables' => array('content' => array(), 'pane' => array(), 'display' => array()),
'path' => drupal_get_path('module', 'panels') . '/templates',
'template' => 'panels-pane',
);
@@ -258,7 +273,6 @@ function panels_init() {
}
ctools_add_css('panels', 'panels');
- ctools_add_js('panels', 'panels');
}
/**
@@ -270,7 +284,7 @@ function panels_permission() {
return array(
'use panels dashboard' => array(
'title' => t("Use Panels Dashboard"),
- 'description' => t("Allows a user to access the !link.", array('!link' => l('Panels Dashboard', 'admin/structure/panels'))),
+ 'description' => t('Allows a user to access the Panels Dashboard.', array('@url' => url('admin/structure/panels'))),
),
'view pane admin links' => array( // @todo
'title' => t("View administrative links on Panel panes"),
@@ -308,6 +322,10 @@ function panels_permission() {
'title' => t('Use panel locks'),
'description' => t('Allows a user to lock and unlock panes in a panel display.'),
),
+ 'use ipe with page manager' => array(
+ 'title' => t("Use the Panels In-Place Editor with Page Manager"),
+ 'description' => t('Allows users with access to the In-Place editor to administer page manager pages. This permission is only needed for users without "use page manager" access.'),
+ ),
);
}
@@ -322,6 +340,15 @@ function panels_permission() {
// $legacy->determineStatus();
//}
+/**
+ * Implements hook_flush_caches().
+ */
+function panels_flush_caches() {
+ if (db_table_exists('cache_panels')) {
+ return array('cache_panels');
+ }
+}
+
// ---------------------------------------------------------------------------
// CTools hook implementations
//
@@ -652,8 +679,10 @@ class panels_display {
$pane->panel = $location;
}
- // Get a temporary pid for this pane.
- $pane->pid = "new-" . $this->next_new_pid();
+ // Generate a permanent uuid for this pane, and use
+ // it as a temporary pid.
+ $pane->uuid = ctools_uuid_generate();
+ $pane->pid = 'new-' . $pane->uuid;
// Add the pane to the approprate spots.
$this->content[$pane->pid] = &$pane;
@@ -667,23 +696,10 @@ class panels_display {
function clone_pane($pid) {
$pane = clone $this->content[$pid];
+ $pane->uuid = ctools_uuid_generate();
return $pane;
}
- function next_new_pid() {
- // We don't use static vars to record the next new pid because
- // temporary pids can last for years in exports and in caching
- // during editing.
- $id = array(0);
- foreach (array_keys($this->content) as $pid) {
- if (!is_numeric($pid)) {
- $id[] = substr($pid, 4);
- }
- }
- $next_id = max($id);
- return ++$next_id;
- }
-
/**
* Get the title from a display.
*
@@ -787,6 +803,7 @@ function panels_new_pane($type, $subtype, $set_defaults = FALSE) {
$content_subtype = ctools_content_get_subtype($content_type, $subtype);
$pane->configuration = ctools_content_get_defaults($content_type, $content_subtype);
}
+ drupal_alter('panels_new_pane', $pane);
return $pane;
}
@@ -867,10 +884,13 @@ function panels_load_display($did) {
*
* @ingroup mainapi
*
- * Note a new $display only receives a real did once it is run through this function.
- * Until then, it uses a string placeholder, 'new', in place of a real did. The same
- * applies to all new panes (whether on a new $display or not); in addition,
- * panes have sequential numbers appended, of the form 'new-1', 'new-2', etc.
+ * Note that a new $display only receives a real did once it is run through
+ * this function, and likewise for the pid of any new pane.
+ *
+ * Until then, a new display uses a string placeholder, 'new', in place of
+ * a real did, and a new pane (whether on a new $display or not) appends a
+ * universally-unique identifier (which is stored permanently in the 'uuid'
+ * field). This format is also used in place of the real pid for exports.
*
* @param object $display instanceof panels_display \n
* The display object to be saved. Passed by reference so the caller need not use
@@ -880,6 +900,9 @@ function panels_load_display($did) {
*/
function panels_save_display(&$display) {
$update = (isset($display->did) && is_numeric($display->did)) ? array('did') : array();
+ if (empty($display->uuid) || !ctools_uuid_is_valid($display->uuid)) {
+ $display->uuid = ctools_uuid_generate();
+ }
drupal_write_record('panels_display', $display, $update);
$pids = array();
@@ -910,11 +933,26 @@ function panels_save_display(&$display) {
$pane->did = $display->did;
$old_pid = $pane->pid;
+
+ if (empty($pane->uuid) || !ctools_uuid_is_valid($pane->uuid)) {
+ $pane->uuid = ctools_uuid_generate();
+ }
+
drupal_write_record('panels_pane', $pane, is_numeric($pid) ? array('pid') : array());
+ // Allow other modules to take action after a pane is saved.
+ if ($pane->pid == $old_pid) {
+ module_invoke_all('panels_pane_update', $pane);
+ }
+ else {
+ module_invoke_all('panels_pane_insert', $pane);
+ }
+
if ($pane->pid != $old_pid) {
- // and put it back so our pids and positions can be used
- unset($display->content[$id]);
+ // Remove the old new-* entry from the displays content.
+ unset($display->content[$pid]);
+
+ // and put it back so our pids and positions can be used.
$display->content[$pane->pid] = $pane;
// If the title pane was one of our panes that just got its ID changed,
@@ -945,6 +983,8 @@ function panels_save_display(&$display) {
$display->panels[$id] = $new_panes;
}
if (!empty($pids)) {
+ // Allow other modules to take action before a panes are deleted.
+ module_invoke_all('panels_pane_delete', $pids);
db_delete('panels_pane')->condition('pid', $pids)->execute();
}
@@ -978,6 +1018,7 @@ function panels_delete_display($display) {
else {
$did = $display;
}
+ module_invoke_all('panels_delete_display', $did);
db_delete('panels_display')->condition('did', $did)->execute();
db_delete('panels_pane')->condition('did', $did)->execute();
}
@@ -987,9 +1028,11 @@ function panels_delete_display($display) {
*
* This function is primarily intended as a mechanism for cloning displays.
* It generates an exact replica (in code) of the provided $display, with
- * the exception that it replaces all ids (dids and pids) with 'new-*' values.
- * Only once panels_save_display() is called on the code version of $display will
- * the exported display written to the database and permanently saved.
+ * the exception that it replaces all ids (dids and pids) with place-holder
+ * values (consisting of the display or pane's uuid, with a 'new-' prefix).
+ *
+ * Only once panels_save_display() is called on the code version of $display
+ * will the exported display be written to the database and permanently saved.
*
* @see panels_page_export() or _panels_page_fetch_display() for sample implementations.
*
@@ -1011,10 +1054,12 @@ function panels_delete_display($display) {
*/
function panels_export_display($display, $prefix = '') {
ctools_include('export');
+ if (empty($display->uuid) || !ctools_uuid_is_valid($display->uuid)) {
+ $display->uuid = ctools_uuid_generate();
+ }
+ $display->did = 'new-' . $display->uuid;
$output = ctools_export_object('panels_display', $display, $prefix);
- $pid_counter = &drupal_static(__FUNCTION__, 0);
-
// Initialize empty properties.
$output .= $prefix . '$display->content = array()' . ";\n";
$output .= $prefix . '$display->panels = array()' . ";\n";
@@ -1024,7 +1069,12 @@ function panels_export_display($display, $prefix = '') {
if (!empty($display->content)) {
$region_counters = array();
foreach ($display->content as $pane) {
- $pid = 'new-' . ++$pid_counter;
+
+ if (!isset($pane->uuid) || !ctools_uuid_is_valid($pane->uuid)) {
+ $pane->uuid = ctools_uuid_generate();
+ }
+ $pid = 'new-' . $pane->uuid;
+
if ($pane->pid == $display->title_pane) {
$title_pid = $pid;
}
@@ -1070,6 +1120,9 @@ function panels_render_display(&$display, $renderer = NULL) {
if (!empty($display->context)) {
if ($form_context = ctools_context_get_form($display->context)) {
$form_context->form['#theme'] = 'panels_render_display_form';
+ if (empty($form_context->form['#theme_wrappers']) || !in_array('form', $form_context->form['#theme_wrappers'])) {
+ $form_context['#theme_wrappers'][] = 'form';
+ }
$form_context->form['#display'] = &$display;
return $form_context->form;
}
@@ -1086,10 +1139,7 @@ function panels_render_display(&$display, $renderer = NULL) {
* then operate as a theme function of the form.
*/
function theme_panels_render_display_form($vars) {
- // @todo this is probably broken in D7
- $render = $vars['element']['#display']->render();
- $vars['element']['#children'] = $render;
- return theme('form', $vars);
+ return $vars['element']['#display']->render();
}
// @layout
@@ -1226,7 +1276,9 @@ function template_preprocess_panels_pane(&$vars) {
}
$element = contextual_pre_render_links($element);
- $links += $element['#links'];
+ if(!empty($element['#links'])) {
+ $links += $element['#links'];
+ }
}
if ($links) {
@@ -1276,12 +1328,13 @@ function template_preprocess_panels_pane(&$vars) {
// Add template file suggestion for content type and sub-type.
$vars['theme_hook_suggestions'][] = $base . $delimiter . $content->type;
- $vars['theme_hook_suggestions'][] = $base . $delimiter . strtr($content->type, '-', '_') . $delimiter . strtr($content->subtype, '-', '_');
+ $vars['theme_hook_suggestions'][] = $base . $delimiter . strtr(ctools_cleanstring($content->type, array('lower case' => TRUE)), '-', '_') . $delimiter . strtr(ctools_cleanstring($content->subtype, array('lower case' => TRUE)), '-', '_');
$vars['pane_prefix'] = !empty($content->pane_prefix) ? $content->pane_prefix : '';
$vars['pane_suffix'] = !empty($content->pane_suffix) ? $content->pane_suffix : '';
$vars['title'] = !empty($content->title) ? $content->title : '';
+ $vars['title_heading'] = !empty($content->title_heading) ? $content->title_heading : 'h2';
$vars['title_attributes_array']['class'][] = 'pane-title';
$vars['feeds'] = !empty($content->feeds) ? implode(' ', $content->feeds) : '';
@@ -1526,7 +1579,7 @@ function panels_edit_cache_break_lock($cache) {
* Get display edit cache on behalf of panel context.
*
* The key is the second half of the key in this form:
- * panel_context:TASK_NAME:HANDLER_NAME;
+ * panel_context:TASK_NAME::HANDLER_NAME::args::url;
*/
function panel_context_panels_cache_get($key) {
ctools_include('common', 'panels');
@@ -1535,7 +1588,7 @@ function panel_context_panels_cache_get($key) {
// this loads the panel context inc even if we don't use the plugin.
$plugin = page_manager_get_task_handler('panel_context');
- list($task_name, $handler_name) = explode(':', $key, 2);
+ list($task_name, $handler_name, $args, $q) = explode('::', $key, 4);
$page = page_manager_get_page_cache($task_name);
if (isset($page->display_cache[$handler_name])) {
return $page->display_cache[$handler_name];
@@ -1549,8 +1602,20 @@ function panel_context_panels_cache_get($key) {
}
$cache = new stdClass();
+ $task = page_manager_get_task($page->task_id);
+ //ctools_context_handler_get_all_contexts($page->task, $page->subtask, $handler);
+ $arguments = array();
+ if ($args) {
+ $arguments = explode('\\', $args);
+ $contexts = ctools_context_handler_get_task_contexts($task, $page->subtask, $arguments);
+ $contexts = ctools_context_handler_get_handler_contexts($contexts, $handler);
+ }
+ else {
+ $contexts = ctools_context_handler_get_all_contexts($page->task, $page->subtask, $handler);
+ }
+
$cache->display = &panels_panel_context_get_display($handler);
- $cache->display->context = ctools_context_handler_get_all_contexts($page->task, $page->subtask, $handler);
+ $cache->display->context = $contexts;
$cache->display->cache_key = 'panel_context:' . $key;
$cache->content_types = panels_common_get_allowed_types('panels_page', $cache->display->context);
$cache->display_title = TRUE;
@@ -1563,7 +1628,7 @@ function panel_context_panels_cache_get($key) {
* Get the Page Manager cache for the panel_context plugin.
*/
function _panel_context_panels_cache_get_page_cache($key, $cache) {
- list($task_name, $handler_name) = explode(':', $key, 2);
+ list($task_name, $handler_name, $args, $q) = explode('::', $key, 4);
$page = page_manager_get_page_cache($task_name);
$page->display_cache[$handler_name] = $cache;
if ($handler_name) {
@@ -1726,6 +1791,98 @@ function _panels_builder_filter($layout) {
return empty($layout['builder']);
}
+/**
+ * Implements hook_get_pane_links_alter().
+ *
+ * add links to the Panels pane dropdown menu.
+ */
+function panels_get_pane_links_alter(&$links, $pane, $content_type) {
+ if ($pane->type === "block"){
+ $prefixed_name = $pane->subtype;
+
+ // breakup the subtype string into parts.
+ $exploded_subtype = explode('-', $pane->subtype);
+
+ // get the first part of the string.
+ $subtype_prefix = $exploded_subtype[0];
+
+ // get the first part of the string and add a hyphen.
+ $subtype_prefix_hyphen = $exploded_subtype[0] . '-';
+
+ // remove the prefix block- to get the name.
+ $name_of_block = ltrim( $prefixed_name, $subtype_prefix_hyphen);
+
+ // check for user added menus created at /admin/structure/menu/add
+ // menus of that type have a subtype that is prefixed with menu-menu-
+ if (substr($prefixed_name, 0, 10) === "menu-menu-"){
+ // remove the first prefix menu- from menu-menu- to get the name.
+ $name_of_block = substr($prefixed_name, 5);
+
+ $links['top'][] = array(
+ 'title' => t('Edit block'),
+ 'href' => url('admin/structure/block/manage/' . $subtype_prefix . '/' . $name_of_block . '/configure', array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+
+ $links['top'][] = array(
+ 'title' => t('Edit menu links'),
+ 'href' => url('admin/structure/menu/manage/' . $name_of_block, array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+ }
+
+ // check for module provided menu blocks like Devels or Features
+ // menus of that type have a subtype that is prefixed with menu-
+ elseif(substr($prefixed_name, 0, 5) === "menu-"){
+ // remove the first prefix menu- to get the name.
+ $name_of_block = substr($prefixed_name, 5);
+
+ $links['top'][] = array(
+ 'title' => t('Edit block'),
+ 'href' => url('admin/structure/block/manage/' . $subtype_prefix . '/' . $name_of_block . '/configure', array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+
+ $links['top'][] = array(
+ 'title' => t('Edit menu links'),
+ 'href' => url('admin/structure/menu/manage/' . $name_of_block, array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+ }
+
+ // check for system blocks with menu links
+ elseif(substr($prefixed_name, 0, 7) === "system-") {
+ // remove the first prefix system- to get the name
+ $name_of_block = substr($prefixed_name, 7);
+
+ $names_of_system_menus = menu_list_system_menus();
+
+ $links['top'][] = array(
+ 'title' => t('Edit block'),
+ 'href' => url('admin/structure/block/manage/' . $subtype_prefix . '/' . $name_of_block . '/configure', array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+
+ if (array_key_exists($name_of_block, $names_of_system_menus)){
+ $links['top'][] = array(
+ 'title' => t('Edit menu links'),
+ 'href' => url('admin/structure/menu/manage/' . $name_of_block, array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+ }
+ }
+
+ // for all other blocks without menus
+ else{
+ $links['top'][] = array(
+ 'title' => t('Edit block'),
+ 'href' => url('admin/structure/block/manage/' . $subtype_prefix . '/' . $name_of_block . '/configure', array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+ }
+ }
+}
+
// --------------------------------------------------------------------------
// Deprecated functions
//
@@ -1745,7 +1902,7 @@ function panels_get_path($file, $base_path = FALSE, $module = 'panels') {
function panels_preprocess_html(&$vars) {
$panel_body_css = &drupal_static('panel_body_css');
if (!empty($panel_body_css['body_classes_to_remove'])) {
- $classes_to_remove = explode(' ', $panel_body_css['body_classes_to_remove']);
+ $classes_to_remove = array_filter(explode(' ', $panel_body_css['body_classes_to_remove']), 'strlen');
foreach ($vars['classes_array'] as $key => $css_class) {
if (in_array($css_class, $classes_to_remove)) {
unset($vars['classes_array'][$key]);
diff --git a/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe-rtl.css b/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe-rtl.css
new file mode 100644
index 00000000..abfc50e1
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe-rtl.css
@@ -0,0 +1,67 @@
+
+div.panels-ipe-handlebar-wrapper ul {
+ float: right;
+ text-align: left;
+}
+
+div.panels-ipe-handlebar-wrapper li {
+ margin: 0 0 0 .5em;
+ float: right;
+}
+
+div.panels-ipe-draghandle span.panels-ipe-draghandle-icon {
+ float: left;
+}
+
+div.panels-ipe-placeholder {
+ text-align: right;
+}
+
+div.panels-ipe-newblock {
+ left: 30px;
+ right: auto;
+}
+
+div.panels-ipe-handlebar-wrapper li a span,
+div.panels-ipe-newblock a span {
+ text-align: right;
+}
+
+div.panels-ipe-newblock a.style {
+ margin-left: .5em;
+ margin-right: auto;
+}
+
+.panels-ipe-editing .panels-ipe-region {
+ float: right;
+}
+
+/** ============================================================================
+ * Controller form markup
+ */
+
+.ipe-throbber {
+ right: 49%;
+ right: auto;
+}
+
+div.panels-ipe-control .form-submit {
+ padding: 0 34px 2px 0.8em;
+}
+
+#panels-ipe-save,
+#panels-ipe-cancel {
+ background-position: 86% 0;
+}
+
+div.panels-ipe-pseudobutton-container a.panels-ipe-startedit {
+ padding-right: 34px;
+ padding-left: 10px;
+ background-position: 93% 9px;
+}
+
+div.panels-ipe-pseudobutton-container a.panels-ipe-change-layout {
+ padding-right: 34px;
+ padding-left: 10px;
+ background-position: 93% 9px;
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe.css b/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe.css
index 7d71db06..e7896e26 100644
--- a/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe.css
+++ b/sites/all/modules/contrib/panels/panels/panels_ipe/css/panels_ipe.css
@@ -27,6 +27,7 @@ div.panels-ipe-handlebar-wrapper {
.panels-ipe-editing div.panels-ipe-portlet-wrapper {
margin-top: 1em;
border: 1px solid #CCC;
+ width: 100%;
}
/* Hide empty panes when not editing them. */
@@ -163,6 +164,10 @@ div.panels-ipe-newblock {
z-index: 99;
}
+div.panels-ipe-newblock li {
+ padding: 0;
+}
+
div.panels-ipe-handlebar-wrapper li a,
div.panels-ipe-dragtitle span,
div.panels-ipe-newblock a,
@@ -236,7 +241,9 @@ div.panels-ipe-handlebar-wrapper li.delete a span {
div.panels-ipe-handlebar-wrapper li a:hover,
div.panels-ipe-dragtitle span:hover,
div.panels-ipe-newblock a:hover,
-span.panels-ipe-draghandle-icon:hover {
+span.panels-ipe-draghandle-icon:hover,
+div.panels-ipe-handlebar-wrapper li a:focus,
+div.panels-ipe-newblock a:focus {
background: #E6E6E6;
background-image: linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
background-image: -o-linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
@@ -375,9 +382,10 @@ div.panels-ipe-control .form-submit {
padding: 0 0.8em 2px 34px;
}
-input#panels-ipe-save, input#panels-ipe-cancel,
-input#panels-ipe-save:hover, input#panels-ipe-cancel:hover,
-input#panels-ipe-save:active, input#panels-ipe-cancel:active {
+div.panels-ipe-control input.panels-ipe-save, div.panels-ipe-control input.panels-ipe-cancel,
+div.panels-ipe-control input.panels-ipe-save:hover, div.panels-ipe-control input.panels-ipe-cancel:hover,
+div.panels-ipe-control input.panels-ipe-save:focus, div.panels-ipe-control input.panels-ipe-cancel:focus,
+div.panels-ipe-control input.panels-ipe-save:active, div.panels-ipe-control input.panels-ipe-cancel:active {
background-repeat: no-repeat;
}
@@ -389,7 +397,7 @@ div.panels-ipe-pseudobutton-container a {
text-decoration: none;
}
-input#panels-ipe-save {
+div.panels-ipe-control .panels-ipe-save {
background-image: url(../images/icon-save.png);
background-image: url(../images/icon-save.png), linear-gradient(bottom, #383838 0%, #666666 100%);
background-image: url(../images/icon-save.png), -o-linear-gradient(bottom, #383838 0%, #666666 100%);
@@ -406,7 +414,7 @@ input#panels-ipe-save {
);
}
-input#panels-ipe-cancel {
+div.panels-ipe-control .panels-ipe-cancel {
background-image: url(../images/icon-close.png);
background-image: url(../images/icon-close.png), linear-gradient(bottom, #383838 0%, #666666 100%);
background-image: url(../images/icon-close.png), -o-linear-gradient(bottom, #383838 0%, #666666 100%);
@@ -424,7 +432,9 @@ input#panels-ipe-cancel {
}
div.panels-ipe-pseudobutton-container:hover,
-div.panels-ipe-control .form-submit:hover {
+div.panels-ipe-control .form-submit:hover,
+div.panels-ipe-pseudobutton-container:focus,
+div.panels-ipe-control .form-submit:focus {
background: #999999;
background-image: linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
background-image: -o-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
@@ -442,11 +452,13 @@ div.panels-ipe-control .form-submit:hover {
color: #FFF;
}
-div.panels-ipe-pseudobutton-container a:hover {
+div.panels-ipe-pseudobutton-container a:hover,
+div.panels-ipe-pseudobutton-container a:focus {
color: #FFF;
}
-input#panels-ipe-cancel:hover {
+div.panels-ipe-control .panels-ipe-cancel:hover,
+div.panels-ipe-control .panels-ipe-cancel:focus {
background-image: url(../images/icon-close.png), linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
background-image: url(../images/icon-close.png), -o-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
background-image: url(../images/icon-close.png), -moz-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
@@ -462,7 +474,8 @@ input#panels-ipe-cancel:hover {
);
}
-input#panels-ipe-save:hover {
+div.panels-ipe-control .panels-ipe-save:hover,
+div.panels-ipe-control .panels-ipe-save:focus {
background-image: url(../images/icon-save.png), linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
background-image: url(../images/icon-save.png), -o-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
background-image: url(../images/icon-save.png), -moz-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
@@ -502,7 +515,7 @@ div.panels-ipe-pseudobutton-container a:active {
color: #CCC;
}
-input#panels-ipe-cancel:active {
+div.panels-ipe-control .panels-ipe-cancel:active {
background-image: url(../images/icon-close.png), linear-gradient(bottom, #616161 0%, #333333 100%);
background-image: url(../images/icon-close.png), -o-linear-gradient(bottom, #616161 0%, #333333 100%);
background-image: url(../images/icon-close.png), -moz-linear-gradient(bottom, #616161 0%, #333333 100%);
@@ -518,7 +531,7 @@ input#panels-ipe-cancel:active {
);
}
-input#panels-ipe-save:active {
+div.panels-ipe-control .panels-ipe-save:active {
background-image: url(../images/icon-save.png), linear-gradient(bottom, #616161 0%, #333333 100%);
background-image: url(../images/icon-save.png), -o-linear-gradient(bottom, #616161 0%, #333333 100%);
background-image: url(../images/icon-save.png), -moz-linear-gradient(bottom, #616161 0%, #333333 100%);
@@ -534,6 +547,12 @@ input#panels-ipe-save:active {
);
}
+div.panels-ipe-control .panels-ipe-save, div.panels-ipe-control .panels-ipe-cancel,
+div.panels-ipe-control .panels-ipe-save:hover, div.panels-ipe-control .panels-ipe-cancel:hover,
+div.panels-ipe-control .panels-ipe-save:active, div.panels-ipe-control .panels-ipe-cancel:active {
+ background-repeat: no-repeat;
+}
+
div.panels-ipe-pseudobutton-container a.panels-ipe-startedit {
padding-left: 34px;
background: url(../images/icon-configure.png) no-repeat 10px 9px;
@@ -552,3 +571,7 @@ div.panels-ipe-button-container {
form#panels-ipe-edit-control-form {
text-align: center;
}
+
+.panels-ipe-dragbar-admin-title{
+ font-size: 0.9em;
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels_ipe/js/panels_ipe.js b/sites/all/modules/contrib/panels/panels/panels_ipe/js/panels_ipe.js
index ec347805..b203f5e5 100644
--- a/sites/all/modules/contrib/panels/panels/panels_ipe/js/panels_ipe.js
+++ b/sites/all/modules/contrib/panels/panels/panels_ipe/js/panels_ipe.js
@@ -15,9 +15,11 @@ Drupal.PanelsIPE = {
$('a.pane-delete:not(.pane-delete-processed)', context)
.addClass('pane-delete-processed')
.click(function() {
- if (confirm('Remove this pane?')) {
+ if (confirm(Drupal.t('Remove this pane?'))) {
$(this).parents('div.panels-ipe-portlet-wrapper').fadeOut('medium', function() {
+ var $sortable = $(this).closest('.ui-sortable');
$(this).empty().remove();
+ $sortable.trigger('sortremove');
});
$(this).parents('div.panels-ipe-display-container').addClass('changed');
}
@@ -34,6 +36,10 @@ Drupal.behaviors.PanelsIPE = {
$('div#panels-ipe-display-' + key + ':not(.panels-ipe-processed)')
.addClass('panels-ipe-processed')
.each(function() {
+ // If we're replacing an old IPE, clean it up a little.
+ if (Drupal.PanelsIPE.editors[key]) {
+ Drupal.PanelsIPE.editors[key].editing = false;
+ }
Drupal.PanelsIPE.editors[key] = new DrupalPanelsIPE(key);
Drupal.PanelsIPE.editors[key].showContainer();
});
@@ -92,6 +98,22 @@ function DrupalPanelsIPE(cache_key, cfg) {
}
});
+
+ // If a user navigates away from a locked IPE, cancel the lock in the background.
+ $(window).bind('beforeunload', function() {
+ if (!ipe.editing) {
+ return;
+ }
+
+ if (ipe.topParent && ipe.topParent.hasClass('changed')) {
+ ipe.changed = true;
+ }
+
+ if (ipe.changed) {
+ return Drupal.t('This will discard all unsaved changes. Are you sure?');
+ }
+ });
+
// If a user navigates away from a locked IPE, cancel the lock in the background.
$(window).bind('unload', function() {
ipe.cancelLock(true);
@@ -170,6 +192,7 @@ function DrupalPanelsIPE(cache_key, cfg) {
};
this.initEditing = function(formdata) {
+ ipe.editing = true;
ipe.topParent = $('div#panels-ipe-display-' + cache_key);
ipe.backup = this.topParent.clone();
@@ -192,7 +215,7 @@ function DrupalPanelsIPE(cache_key, cfg) {
$('.panels-ipe-form-container', ipe.control).append(formdata);
- $('input:submit:not(.ajax-processed)', ipe.control).addClass('ajax-processed').each(function() {
+ $('input:submit:not(.ajax-processed), button:not(.ajax-processed)', ipe.control).addClass('ajax-processed').each(function() {
var element_settings = {};
element_settings.url = $(this.form).attr('action');
@@ -212,17 +235,6 @@ function DrupalPanelsIPE(cache_key, cfg) {
ipe.showForm();
ipe.topParent.addClass('panels-ipe-editing');
- //Reposition the "Add new pane" button
- $('.panels-ipe-newblock').each(function() {
- var link_width_half = parseInt($(this).children('a').outerWidth() / 2);
-
- $(this).css('margin-left', '-' + link_width_half + 'px');
-
- $(this).css('margin-top', '-' + parseInt($(this).children('a').outerHeight() / 2) + 'px');
-
- $(this).parents('.panels-ipe-placeholder').find('h3').css('width', parseInt(($(this).parents('.panels-ipe-placeholder').width() / 2) - link_width_half) + 'px');
- });
-
};
this.hideContainer = function() {
@@ -246,18 +258,18 @@ function DrupalPanelsIPE(cache_key, cfg) {
};
this.endEditing = function() {
+ ipe.editing = false;
ipe.lockPath = null;
- $('.panels-ipe-form-container', ipe.control).empty();
+ $('.panels-ipe-form-container').empty();
// Re-show all the IPE non-editing meta-elements
$('div.panels-ipe-off').show('fast');
ipe.showButtons();
// Re-hide all the IPE meta-elements
$('div.panels-ipe-on').hide();
- if (ipe.topParent) {
- ipe.topParent.removeClass('panels-ipe-editing');
- $('div.panels-ipe-sort-container', ipe.topParent).sortable("destroy");
- }
+
+ $('.panels-ipe-editing').removeClass('panels-ipe-editing');
+ $('div.panels-ipe-sort-container.ui-sortable', ipe.topParent).sortable("destroy");
};
this.saveEditing = function() {
@@ -273,7 +285,7 @@ function DrupalPanelsIPE(cache_key, cfg) {
val += id;
}
});
- $('input[name="panel[pane][' + region + ']"]', ipe.control).val(val);
+ $('[name="panel[pane][' + region + ']"]', ipe.control).val(val);
});
}
@@ -310,7 +322,7 @@ function DrupalPanelsIPE(cache_key, cfg) {
this.createSortContainers = function() {
$('div.panels-ipe-region', this.topParent).each(function() {
- $('div.panels-ipe-portlet-marker', this).parent()
+ $(this).children('div.panels-ipe-portlet-marker').parent()
.wrapInner('');
// Move our gadgets outside of the sort container so that sortables
@@ -318,9 +330,6 @@ function DrupalPanelsIPE(cache_key, cfg) {
$('div.panels-ipe-portlet-static', this).each(function() {
$(this).prependTo($(this).parent().parent());
});
-
- // Also remove the last panel separator.
- $('div.panel-separator', this).filter(':last').remove();
});
}
@@ -334,6 +343,8 @@ $(function() {
Drupal.PanelsIPE.editors[data.key].initEditing(data.data);
Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath;
}
+ Drupal.attachBehaviors();
+
};
Drupal.ajax.prototype.commands.IPEsetLockState = function(ajax, data, status) {
@@ -342,6 +353,12 @@ $(function() {
}
};
+ Drupal.ajax.prototype.commands.addNewPane = function(ajax, data, status) {
+ if (Drupal.PanelsIPE.editors[data.key]) {
+ Drupal.PanelsIPE.editors[data.key].changed = true;
+ }
+ };
+
Drupal.ajax.prototype.commands.cancelIPE = function(ajax, data, status) {
if (Drupal.PanelsIPE.editors[data.key]) {
Drupal.PanelsIPE.editors[data.key].cancelIPE();
@@ -366,12 +383,45 @@ $(function() {
}
};
+ Drupal.ajax.prototype.commands.insertNewPane = function(ajax, data, status) {
+ IPEContainerSelector = '#panels-ipe-regionid-' + data.regionId + ' div.panels-ipe-sort-container';
+ firstPaneSelector = IPEContainerSelector + ' div.panels-ipe-portlet-wrapper:first';
+ // Insert the new pane before the first existing pane in the region, if
+ // any.
+ if ($(firstPaneSelector).length) {
+ insertData = {
+ 'method': 'before',
+ 'selector': firstPaneSelector,
+ 'data': data.renderedPane,
+ 'settings': null
+ }
+ Drupal.ajax.prototype.commands.insert(ajax, insertData, status);
+ }
+ // Else, insert it as a first child of the container. Doing so might fall
+ // outside of the wrapping markup for the style, but it's the best we can
+ // do.
+ else {
+ insertData = {
+ 'method': 'prepend',
+ 'selector': IPEContainerSelector,
+ 'data': data.renderedPane,
+ 'settings': null
+ }
+ Drupal.ajax.prototype.commands.insert(ajax, insertData, status);
+ }
+ };
+
/**
* Override the eventResponse on ajax.js so we can add a little extra
* behavior.
*/
Drupal.ajax.prototype.ipeReplacedEventResponse = Drupal.ajax.prototype.eventResponse;
Drupal.ajax.prototype.eventResponse = function (element, event) {
+ if (element.ipeCancelThis) {
+ element.ipeCancelThis = null;
+ return false;
+ }
+
if ($(this.element).attr('id') == 'panels-ipe-cancel') {
if (!Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].cancelEditing()) {
return false;
@@ -404,7 +454,7 @@ $(function() {
Drupal.ajax.prototype.ipeReplacedBeforeSerialize = Drupal.ajax.prototype.beforeSerialize;
Drupal.ajax.prototype.beforeSerialize = function (element_settings, options) {
- if ($(this.element).attr('id') == 'panels-ipe-save') {
+ if ($(this.element).hasClass('panels-ipe-save')) {
Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].saveEditing();
};
return this.ipeReplacedBeforeSerialize(element_settings, options);
diff --git a/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.info b/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.info
index 204e6c7c..6ebaf5c9 100644
--- a/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.info
+++ b/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.info
@@ -1,14 +1,15 @@
name = Panels In-Place Editor
description = Provide a UI for managing some Panels directly on the frontend, instead of having to use the backend.
package = "Panels"
+version = PANELS_VERSION
dependencies[] = panels
core = 7.x
configure = admin/structure/panels
files[] = panels_ipe.module
-; Information added by drupal.org packaging script on 2012-08-18
-version = "7.x-3.3"
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-3.5"
core = "7.x"
project = "panels"
-datestamp = "1345319572"
+datestamp = "1422472985"
diff --git a/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.module b/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.module
index 7aeb915f..a11fc3ae 100644
--- a/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.module
+++ b/sites/all/modules/contrib/panels/panels/panels_ipe/panels_ipe.module
@@ -73,18 +73,17 @@ function theme_panels_ipe_placeholder_pane($vars) {
return $output;
}
-function theme_panels_ipe_pane_wrapper($vars) {
- $output = $vars['output'];
+function template_preprocess_panels_ipe_pane_wrapper(&$vars) {
$pane = $vars['pane'];
$display = $vars['display'];
$renderer = $vars['renderer'];
$content_type = ctools_get_content_type($pane->type);
$subtype = ctools_content_get_subtype($content_type, $pane->subtype);
- $links = array();
+ $vars['links'] = array();
if (ctools_content_editable($content_type, $subtype, $pane->configuration)) {
- $links['edit'] = array(
+ $vars['links']['edit'] = array(
'title' => isset($content_type['edit text']) ? '' . $content_type['edit text'] . '' : '' . t('Settings') . '',
'href' => $renderer->get_url('edit-pane', $pane->pid),
'html' => TRUE,
@@ -98,7 +97,7 @@ function theme_panels_ipe_pane_wrapper($vars) {
// Add option to configure style in IPE
if (user_access('administer panels styles')) {
- $links['style'] = array(
+ $vars['links']['style'] = array(
'title' => '' . t('Style') . '',
'href' => $renderer->get_url('style-type', 'pane', $pane->pid),
'html' => TRUE,
@@ -111,7 +110,7 @@ function theme_panels_ipe_pane_wrapper($vars) {
// Deleting is managed entirely in the js; this is just an attachment point
// for it
- $links['delete'] = array(
+ $vars['links']['delete'] = array(
'title' => '' . t('Delete') . '',
'href' => '#',
'html' => TRUE,
@@ -122,19 +121,38 @@ function theme_panels_ipe_pane_wrapper($vars) {
),
);
+ $context = array(
+ 'pane' => $pane,
+ 'display' => $display,
+ 'renderer' => $renderer
+ );
+ drupal_alter('panels_ipe_pane_links', $vars['links'], $context);
+
+}
+
+function theme_panels_ipe_pane_wrapper($vars) {
+ $output = $vars['output'];
+ $pane = $vars['pane'];
+ $display = $vars['display'];
+
$attributes = array(
'class' => 'panels-ipe-linkbar',
);
- $links = theme('links', array('links' => $links, 'attributes' => $attributes));
+ $type = ctools_get_content_type($pane->type);
+ $title =' ';
+
+ $links = theme('links', array('links' => $vars['links'], 'attributes' => $attributes));
+
if (!empty($pane->locks['type']) && $pane->locks['type'] == 'immovable') {
- $links = ' ';
+ $links = ' ';
}
else {
- $links = ' ';
+ $links = ' ';
}
$handlebar = ' ';
+
return $handlebar . $output;
}
@@ -142,33 +160,53 @@ function theme_panels_ipe_region_wrapper($vars) {
return $vars['controls'] . $vars['output'];
}
-function theme_panels_ipe_add_pane_button($vars) {
+function template_preprocess_panels_ipe_add_pane_button(&$vars) {
$region_id = $vars['region_id'];
$display = $vars['display'];
$renderer = $vars['renderer'];
- $link = '';
+ $vars['links'] = '';
// Add option to configure style in IPE
if (user_access('administer panels styles')) {
- $link .= ' ' . l('' . t('Region style') . '', $renderer->get_url('style-type', 'region', $region_id), array(
+ $vars['links']['style'] = array(
+ 'title' => '' . t('Region style') . '',
+ 'href' => $renderer->get_url('style-type', 'region', $region_id),
+ 'html' => TRUE,
'attributes' => array(
- 'class' => array('ctools-use-modal', 'style', 'panels-ipe-hide-bar'),
+ 'class' => array('ctools-use-modal', 'panels-ipe-hide-bar', 'style'),
'title' => t('Region style'),
),
- 'html' => TRUE,
- ));
+ );
}
// Add option to add items in the IPE
- $link .= ' ' . l('' . t('Add new pane') . '', $renderer->get_url('select-content', $region_id), array(
- 'attributes' => array(
- 'class' => array('ctools-use-modal', 'add', 'panels-ipe-hide-bar'),
- 'title' => t('Add new pane'),
- ),
- 'html' => TRUE,
- ));
+ $vars['links']['add-pane'] = array(
+ 'title' => '' . t('Add new pane') . '',
+ 'href' => $renderer->get_url('select-content', $region_id),
+ 'attributes' => array(
+ 'class' => array('ctools-use-modal', 'add', 'panels-ipe-hide-bar'),
+ 'title' => t('Add new pane'),
+ ),
+ 'html' => TRUE,
+ );
- return '' . $link . '';
+ $context = array(
+ 'region_id' => $region_id,
+ 'display' => $display,
+ 'renderer' => $renderer,
+ );
+ drupal_alter('panels_ipe_region_links', $vars['links'], $context);
+
+}
+
+function theme_panels_ipe_add_pane_button($vars) {
+ $attributes = array(
+ 'class' => array('panels-ipe-linkbar', 'inline'),
+ );
+
+ $links = theme('links', array('links' => $vars['links'], 'attributes' => $attributes));
+
+ return '' . $links . '';
}
/**
diff --git a/sites/all/modules/contrib/panels/panels/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php b/sites/all/modules/contrib/panels/panels/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php
index aedc9135..c959a6de 100644
--- a/sites/all/modules/contrib/panels/panels/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php
+++ b/sites/all/modules/contrib/panels/panels/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php
@@ -29,6 +29,7 @@ class panels_renderer_ipe extends panels_renderer_editor {
'#type' => 'link',
'#title' => t('Customize this page'),
'#href' => $this->get_url('save_form'),
+ '#options' => array('query' => drupal_get_destination()),
'#id' => 'panels-ipe-customize-page',
'#attributes' => array(
'class' => array('panels-ipe-startedit', 'panels-ipe-pseudobutton'),
@@ -49,6 +50,7 @@ class panels_renderer_ipe extends panels_renderer_editor {
'#type' => 'link',
'#title' => t('Change layout'),
'#href' => $this->get_url('change_layout'),
+ '#options' => array('query' => drupal_get_destination()),
'#attributes' => array(
'class' => array('panels-ipe-change-layout', 'panels-ipe-pseudobutton', 'ctools-modal-layout'),
),
@@ -134,15 +136,28 @@ class panels_renderer_ipe extends panels_renderer_editor {
return "pid}\" class=\"panels-ipe-portlet-wrapper panels-ipe-portlet-marker\">" . $output . "";
}
+ function prepare_panes($panes) {
+ // Set to admin mode just for this to ensure all panes are represented.
+ $this->admin = TRUE;
+ $panes = parent::prepare_panes($panes);
+ $this->admin = FALSE;
+ }
+
function render_pane_content(&$pane) {
- $content = parent::render_pane_content($pane);
+ if (!empty($pane->shown) && panels_pane_access($pane, $this->display)) {
+ $content = parent::render_pane_content($pane);
+ }
// Ensure that empty panes have some content.
if (empty($content) || empty($content->content)) {
+ if (empty($content)) {
+ $content = new stdClass();
+ }
+
// Get the administrative title.
$content_type = ctools_get_content_type($pane->type);
$title = ctools_content_admin_title($content_type, $pane->subtype, $pane->configuration, $this->display->context);
- $content->content = t('Placeholder for empty "@title"', array('@title' => $title));
+ $content->content = t('Placeholder for empty or inaccessible "@title"', array('@title' => html_entity_decode($title, ENT_QUOTES)));
// Add these to prevent notices.
$content->type = 'panels_ipe';
$content->subtype = 'panels_ipe';
@@ -227,6 +242,7 @@ class panels_renderer_ipe extends panels_renderer_editor {
$_POST['ajax_html_ids'] = array();
$form_state = array(
+ 'renderer' => $this,
'display' => &$this->display,
'content_types' => $this->cache->content_types,
'rerender' => FALSE,
@@ -244,7 +260,7 @@ class panels_renderer_ipe extends panels_renderer_editor {
'command' => 'initIPE',
'key' => $this->clean_key,
'data' => drupal_render($output),
- 'lockPath' => $this->get_url('unlock_ipe'),
+ 'lockPath' => url($this->get_url('unlock_ipe')),
);
return;
}
@@ -315,7 +331,7 @@ class panels_renderer_ipe extends panels_renderer_editor {
$this->commands[] = array(
'command' => 'IPEsetLockState',
'key' => $this->clean_key,
- 'lockPath' => $this->get_url('unlock_ipe'),
+ 'lockPath' => url($this->get_url('unlock_ipe')),
);
}
@@ -344,7 +360,7 @@ class panels_renderer_ipe extends panels_renderer_editor {
if (!empty($form_state['clicked_button']['#save-display'])) {
// Saved. Save the cache.
panels_edit_cache_save($this->cache);
- $this->display->skip_cache;
+ $this->display->skip_cache = TRUE;
// Since the layout changed, we have to update these things in the
// renderer in order to get the right settings.
@@ -391,8 +407,16 @@ class panels_renderer_ipe extends panels_renderer_editor {
$pane = $this->display->content[$pid];
}
- $this->commands[] = ajax_command_prepend("#panels-ipe-regionid-{$pane->panel} div.panels-ipe-sort-container", $this->render_pane($pane));
+ $this->commands[] = array(
+ 'command' => 'insertNewPane',
+ 'regionId' => $pane->panel,
+ 'renderedPane' => $this->render_pane($pane),
+ );
$this->commands[] = ajax_command_changed("#panels-ipe-display-{$this->clean_key}");
+ $this->commands[] = array(
+ 'command' => 'addNewPane',
+ 'key' => $this->clean_key,
+ );
}
}
@@ -430,12 +454,14 @@ function panels_ipe_edit_control_form($form, &$form_state) {
'#type' => 'submit',
'#value' => t('Save'),
'#id' => 'panels-ipe-save',
+ '#attributes' => array('class' => array('panels-ipe-save')),
'#submit' => array('panels_edit_display_form_submit'),
'#save-display' => TRUE,
);
$form['buttons']['cancel'] = array(
'#type' => 'submit',
'#id' => 'panels-ipe-cancel',
+ '#attributes' => array('class' => array('panels-ipe-cancel')),
'#value' => t('Cancel'),
);
return $form;
diff --git a/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.css b/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.css
new file mode 100644
index 00000000..246921c5
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.css
@@ -0,0 +1,12 @@
+/**
+ * @file
+ * Custom CSS for the Mini Panels module.
+ */
+
+/**
+ * Customize the CTools wizard trail / breadcrumb used on the edit pages for
+ * Mini Panels as a stop-gap measure until the UX can be completely re-done.
+ */
+.wizard-trail {
+ text-align: right;
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.info b/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.info
index 96d17b6e..46a6b406 100644
--- a/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.info
+++ b/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.info
@@ -1,12 +1,13 @@
name = Mini panels
description = Create mini panels that can be used as blocks by Drupal and panes by other panel modules.
package = "Panels"
+version = PANELS_VERSION
dependencies[] = panels
core = 7.x
files[] = plugins/export_ui/panels_mini_ui.class.php
-; Information added by drupal.org packaging script on 2012-08-18
-version = "7.x-3.3"
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-3.5"
core = "7.x"
project = "panels"
-datestamp = "1345319572"
+datestamp = "1422472985"
diff --git a/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.module b/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.module
index a0aa662d..f701893a 100644
--- a/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.module
+++ b/sites/all/modules/contrib/panels/panels/panels_mini/panels_mini.module
@@ -112,6 +112,7 @@ function panels_mini_block_view($delta = 0) {
$contexts = ctools_context_match_required_contexts($panel_mini->requiredcontexts, $current_page['contexts']);
}
}
+ drupal_alter('panels_mini_block_contexts', $contexts, $panel_mini);
$panel_mini->context = $panel_mini->display->context = ctools_context_load_contexts($panel_mini, FALSE, $contexts);
$panel_mini->display->css_id = panels_mini_get_id($panel_mini->name);
@@ -130,8 +131,8 @@ function panels_mini_block_view($delta = 0) {
*/
function panels_mini_block_configure($delta = 0) {
return array(
- 'admin-shortcut' => array(
- '#value' => l(t('Manage this mini-panel'), 'admin/structure/mini-panels/' . $delta . '/edit')
+ 'admin_shortcut' => array(
+ '#markup' => l(t('Manage this mini-panel'), 'admin/structure/mini-panels/list/' . $delta . '/edit')
),
);
}
@@ -178,6 +179,44 @@ function panels_mini_block_list_alter(&$blocks) {
}
}
+/**
+ * Implements hook_get_pane_links_alter().
+ */
+function panels_mini_get_pane_links_alter(&$links, $pane, $content_type) {
+ if ($pane->type == 'panels_mini') {
+ $links['top']['edit_panels_mini'] = array(
+ 'title' => t('Edit mini panel'),
+ 'href' => url('admin/structure/mini-panels/list/' . $pane->subtype . '/edit/content', array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+ }
+}
+
+/**
+ * Implements hook_contextual_links_view_alter().
+ */
+function panels_mini_contextual_links_view_alter(&$element, $items) {
+
+ // Add contextual links to all mini panel blocks with bid property.
+ if (isset($element['#element']['#block']) && isset($element['#element']['#block']->bid) && strpos((string) $element['#element']['#block']->bid, 'panels_mini') === 0) {
+
+ $admin_pages = array(
+ t('Configure mini panel settings') => 'basic',
+ t('Configure mini panel context') => 'context',
+ t('Configure mini panel layout') => 'layout',
+ t('Configure mini panel content') => 'content',
+ );
+
+ foreach ($admin_pages as $title => $tail) {
+ $element['#links']['mini-panels-' . $tail] = array(
+ 'title' => $title,
+ 'href' => 'admin/structure/mini-panels/list/' . $element['#element']['#block']->delta . '/edit/' . $tail,
+ 'query' => drupal_get_destination(),
+ );
+ }
+ }
+}
+
/**
* Statically store all used IDs to ensure all mini panels get a unique id.
*/
@@ -285,7 +324,8 @@ function panels_mini_load_all($reset = FALSE) {
}
}
- return $cache;
+ // Strip out NULL entries that may have been added by panels_mini_load().
+ return array_filter($cache);
}
/**
@@ -479,3 +519,14 @@ function panels_mini_panels_dashboard_blocks(&$vars) {
);
}
+
+/**
+ * Implements template_preprocess_ctools_wizard_trail().
+ *
+ * Customize the divider used in the CTools wizard to build the edit pages for
+ * Mini Panels as a stop-gap measure until the UX can be completely re-done.
+ */
+function panels_mini_preprocess_ctools_wizard_trail(&$variables) {
+ $variables['divider'] = ' | ';
+ drupal_add_css(drupal_get_path('module', 'panels_mini') . '/panels_mini.css');
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels_mini/plugins/content_types/panels_mini.inc b/sites/all/modules/contrib/panels/panels/panels_mini/plugins/content_types/panels_mini.inc
index 6bd4d94b..21701113 100644
--- a/sites/all/modules/contrib/panels/panels/panels_mini/plugins/content_types/panels_mini.inc
+++ b/sites/all/modules/contrib/panels/panels/panels_mini/plugins/content_types/panels_mini.inc
@@ -109,6 +109,16 @@ function panels_mini_panels_mini_content_type_render($subtype, $conf, $panel_arg
$block->content = panels_render_display($mini->display);
$block->title = $mini->display->get_title();
+ if (user_access('administer mini panels')) {
+ $block->admin_links = array(
+ array(
+ 'title' => t('Configure mini panel'),
+ 'href' => "admin/structure/mini-panels/list/$subtype/edit/content",
+ 'query' => drupal_get_destination(),
+ ),
+ );
+ }
+
unset($viewing[$mini->name]);
return $block;
}
@@ -137,3 +147,30 @@ function panels_mini_panels_mini_content_type_admin_title($subtype, $conf) {
return $title;
}
+/**
+ * Callback to provide administrative info. Provide links to edit the mini
+ * panel.
+ */
+function panels_mini_panels_mini_content_type_admin_info($subtype, $conf) {
+ $mini = panels_mini_load($subtype);
+ if (!$mini) {
+ return FALSE;
+ }
+
+ $block = new stdClass();
+ $block->title = $mini->admin_title;
+ $admin_pages = array(
+ t('Settings') => 'basic',
+ t('Context') => 'context',
+ t('Layout') => 'layout',
+ t('Content') => 'content',
+ );
+
+ $links = array();
+ foreach ($admin_pages as $title => $tail) {
+ $links[] = l($title, 'admin/structure/mini-panels/list/' . $subtype . '/edit/' . $tail, array('query' => drupal_get_destination()));
+ }
+
+ $block->content = theme('item_list', array('items' => $links));
+ return $block;
+}
diff --git a/sites/all/modules/contrib/panels/panels/panels_mini/plugins/export_ui/panels_mini_ui.class.php b/sites/all/modules/contrib/panels/panels/panels_mini/plugins/export_ui/panels_mini_ui.class.php
index 6c7d0845..cff8fe63 100644
--- a/sites/all/modules/contrib/panels/panels/panels_mini/plugins/export_ui/panels_mini_ui.class.php
+++ b/sites/all/modules/contrib/panels/panels/panels_mini/plugins/export_ui/panels_mini_ui.class.php
@@ -118,6 +118,13 @@ class panels_mini_ui extends ctools_export_ui {
// Get the basic edit form
parent::edit_form($form, $form_state);
+ // Set the admin title machine name length.
+ // We need to do this because the system block name length is
+ // limited to 32 chars.
+ $form['info']['name']['#maxlength'] = 32;
+ $form['info']['name']['#size'] = 34;
+ $form['info']['name']['#description'] .= ' ' . t('The machine name length is limited to 32 characters, due to a limitation in the core block system.');
+
$form['category'] = array(
'#type' => 'textfield',
'#size' => 24,
diff --git a/sites/all/modules/contrib/panels/panels/panels_node/panels_node.info b/sites/all/modules/contrib/panels/panels/panels_node/panels_node.info
index bb63d583..407c9f4f 100644
--- a/sites/all/modules/contrib/panels/panels/panels_node/panels_node.info
+++ b/sites/all/modules/contrib/panels/panels/panels_node/panels_node.info
@@ -1,14 +1,15 @@
name = Panel nodes
description = Create nodes that are divided into areas with selectable content.
package = "Panels"
+version = PANELS_VERSION
dependencies[] = panels
configure = admin/structure/panels
core = 7.x
files[] = panels_node.module
-; Information added by drupal.org packaging script on 2012-08-18
-version = "7.x-3.3"
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-3.5"
core = "7.x"
project = "panels"
-datestamp = "1345319572"
+datestamp = "1422472985"
diff --git a/sites/all/modules/contrib/panels/panels/panels_node/panels_node.module b/sites/all/modules/contrib/panels/panels/panels_node/panels_node.module
index 4044c5a5..894dc672 100644
--- a/sites/all/modules/contrib/panels/panels/panels_node/panels_node.module
+++ b/sites/all/modules/contrib/panels/panels/panels_node/panels_node.module
@@ -339,6 +339,7 @@ function panels_node_hook_view($node, $view_mode) {
$display->css_id = $node->panels_node['css_id'];
// TODO: Find a way to make sure this can't node_view.
$display->context = panels_node_get_context($node);
+ $display->cache_key = 'panels_node:' . $node->nid;
$renderer = panels_get_renderer($node->panels_node['pipeline'], $display);
$node->content['body'] = array(
'#markup' => panels_render_display($display, $renderer),
@@ -429,3 +430,59 @@ function panels_node_panels_dashboard_blocks(&$vars) {
'weight' => -1,
);
}
+
+// ---------------------------------------------------------------------------
+// Callbacks for panel caching.
+
+/**
+ * Get display edit cache for a panel node being edited.
+ *
+ * The key is the second half of the key in this form:
+ * panels_node:NID;
+ */
+function panels_node_panels_cache_get($nid) {
+ ctools_include('object-cache');
+ $cache = ctools_object_cache_get('panels_node_display_cache', $nid);
+ if (empty($cache)) {
+ $cache = new stdClass();
+ $node = node_load($nid);
+ if (empty($node)) {
+ return;
+ }
+
+ ctools_include('common', 'panels');
+ $cache->display = panels_load_display($node->panels_node['did']);
+ $cache->display->css_id = $node->panels_node['css_id'];
+ $cache->display->context = panels_node_get_context($node);
+ $cache->display->cache_key = 'panels_node:' . $node->nid;
+ $cache->content_types = panels_common_get_allowed_types('panels_node', $cache->display->context);
+ $cache->allwed_layouts = panels_common_get_allowed_layouts('panels_node');
+ }
+
+ return $cache;
+}
+
+/**
+ * Store a display edit in progress in the panels cache.
+ */
+function panels_node_panels_cache_set($nid, $cache) {
+ ctools_include('object-cache');
+ ctools_object_cache_set('panels_node_display_cache', $nid, $cache);
+}
+
+/**
+ * Clear all changes made to a display using the panels cache.
+ */
+function panels_node_panels_cache_clear($nid, $cache) {
+ ctools_include('object-cache');
+ ctools_object_cache_clear('panels_node_display_cache', $nid);
+}
+
+/**
+ * React to a cache save and save the display and clear cache.
+ */
+function panels_node_panels_cache_save($nid, $cache) {
+ panels_save_display($cache->display);
+ ctools_include('object-cache');
+ ctools_object_cache_clear('panels_node_display_cache', $nid);
+}
diff --git a/sites/all/modules/contrib/panels/panels/plugins/cache/simple.inc b/sites/all/modules/contrib/panels/panels/plugins/cache/simple.inc
new file mode 100644
index 00000000..6b76678c
--- /dev/null
+++ b/sites/all/modules/contrib/panels/panels/plugins/cache/simple.inc
@@ -0,0 +1,161 @@
+ t("Simple cache"),
+ 'description' => t('Simple caching is a time-based cache. This is a hard limit, and once cached it will remain that way until the time limit expires.'),
+ 'cache get' => 'panels_simple_cache_get_cache',
+ 'cache set' => 'panels_simple_cache_set_cache',
+ 'cache clear' => 'panels_simple_cache_clear_cache',
+ 'settings form' => 'panels_simple_cache_settings_form',
+ 'settings form submit' => 'panels_simple_cache_settings_form_submit',
+ 'defaults' => array(
+ 'lifetime' => 15,
+ 'granularity' => 'none',
+ ),
+);
+
+/**
+ * Get cached content.
+ */
+function panels_simple_cache_get_cache($conf, $display, $args, $contexts, $pane = NULL) {
+ $cid = panels_simple_cache_get_id($conf, $display, $args, $contexts, $pane);
+ $cache = cache_get($cid, 'cache_panels');
+ if (!$cache) {
+ return FALSE;
+ }
+
+ if ((time() - $cache->created) > $conf['lifetime']) {
+ return FALSE;
+ }
+
+ return $cache->data;
+}
+
+/**
+ * Set cached content.
+ */
+function panels_simple_cache_set_cache($conf, $content, $display, $args, $contexts, $pane = NULL) {
+ $cid = panels_simple_cache_get_id($conf, $display, $args, $contexts, $pane);
+ cache_set($cid, $content, 'cache_panels');
+}
+
+/**
+ * Clear cached content.
+ *
+ * Cache clears are always for an entire display, regardless of arguments.
+ */
+function panels_simple_cache_clear_cache($display) {
+ $cid = 'panels_simple_cache';
+
+ // If the panel is stored in the database it'll have a numeric did value.
+ if (is_numeric($display->did)) {
+ $cid .= ':' . $display->did;
+ }
+ // Exported panels won't have a numeric did but may have a usable cache_key.
+ elseif (!empty($display->cache_key)) {
+ $cid .= ':' . str_replace('panel_context:', '', $display->cache_key);
+ }
+ // Alternatively use the css_id.
+ elseif (!empty($display->css_id)) {
+ $cid .= ':' . $display->css_id;
+ }
+ // Failover to just appending the did, which may be the completely unusable
+ // string 'new'.
+ else {
+ $cid .= ':' . $display->did;
+ }
+
+ cache_clear_all($cid, 'cache_panels', TRUE);
+}
+
+/**
+ * Figure out an id for our cache based upon input and settings.
+ */
+function panels_simple_cache_get_id($conf, $display, $args, $contexts, $pane) {
+ $id = 'panels_simple_cache';
+
+ // If the panel is stored in the database it'll have a numeric did value.
+ if (is_numeric($display->did)) {
+ $id .= ':' . $display->did;
+ }
+ // Exported panels won't have a numeric did but may have a usable cache_key.
+ elseif (!empty($display->cache_key)) {
+ $id .= ':' . str_replace('panel_context:', '', $display->cache_key);
+ }
+ // Alternatively use the css_id.
+ elseif (!empty($display->css_id)) {
+ $id .= ':' . $display->css_id;
+ }
+ // Failover to just appending the did, which may be the completely unusable
+ // string 'new'.
+ else {
+ $id .= ':' . $display->did;
+ }
+
+ if ($pane) {
+ $id .= ':' . $pane->pid;
+ }
+
+ if (user_access('view pane admin links')) {
+ $id .= ':admin';
+ }
+
+ switch ($conf['granularity']) {
+ case 'args':
+ foreach ($args as $arg) {
+ $id .= ':' . $arg;
+ }
+ break;
+
+ case 'context':
+ if (!is_array($contexts)) {
+ $contexts = array($contexts);
+ }
+ foreach ($contexts as $context) {
+ if (isset($context->argument)) {
+ $id .= ':' . $context->argument;
+ }
+ }
+ }
+ if (module_exists('locale')) {
+ global $language;
+ $id .= ':' . $language->language;
+ }
+
+ if(!empty($pane->configuration['use_pager']) && !empty($_GET['page'])) {
+ $id .= ':p' . check_plain($_GET['page']);
+ }
+
+ return $id;
+}
+
+function panels_simple_cache_settings_form($conf, $display, $pid) {
+ $options = drupal_map_assoc(array(15, 30, 60, 120, 180, 240, 300, 600, 900, 1200, 1800, 3600, 7200, 14400, 28800, 43200, 86400, 172800, 259200, 345600, 604800), 'format_interval');
+ $form['lifetime'] = array(
+ '#title' => t('Lifetime'),
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $conf['lifetime'],
+ );
+
+ $form['granularity'] = array(
+ '#title' => t('Granularity'),
+ '#type' => 'select',
+ '#options' => array(
+ 'args' => t('Arguments'),
+ 'context' => t('Context'),
+ 'none' => t('None'),
+ ),
+ '#description' => t('If "arguments" are selected, this content will be cached per individual argument to the entire display; if "contexts" are selected, this content will be cached per unique context in the pane or display; if "neither" there will be only one cache for this pane.'),
+ '#default_value' => $conf['granularity'],
+ );
+
+ return $form;
+}
+
diff --git a/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_editor.class.php b/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_editor.class.php
index 7aa9700b..a814eb7f 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_editor.class.php
+++ b/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_editor.class.php
@@ -153,7 +153,7 @@ class panels_renderer_editor extends panels_renderer_standard {
$output = '';
- if (!$block->title) {
+ if (empty($block->title)) {
$block->title = t('No title');
}
@@ -161,7 +161,7 @@ class panels_renderer_editor extends panels_renderer_standard {
if ($buttons) {
$output .= '' . $title . '';
+ $output .= '' . $title . '';
$output .= ''; // grabber
$output .= ' ';
}
- $output .= '';
@@ -186,12 +186,12 @@ class panels_renderer_editor extends panels_renderer_standard {
$style_title = isset($style['title']) ? $style['title'] : t('Default');
- $style_links[] = array(
+ $style_links['title'] = array(
'title' => $style_title,
'attributes' => array('class' => array('panels-text')),
);
- $style_links[] = array(
+ $style_links['change'] = array(
'title' => t('Change'),
'href' => $this->get_url('style-type', $type, $id),
'attributes' => array('class' => array('ctools-use-modal')),
@@ -199,7 +199,7 @@ class panels_renderer_editor extends panels_renderer_standard {
$function = $type != 'pane' ? 'settings form' : 'pane settings form';
if (panels_plugin_get_function('styles', $style, $function)) {
- $style_links[] = array(
+ $style_links['settings'] = array(
'title' => t('Settings'),
'href' => $this->get_url('style-settings', $type, $id),
'attributes' => array('class' => array('ctools-use-modal')),
@@ -307,14 +307,14 @@ class panels_renderer_editor extends panels_renderer_standard {
$links = array();
if (!empty($pane->shown)) {
- $links[] = array(
+ $links['top']['disabled'] = array(
'title' => t('Disable this pane'),
'href' => $this->get_url('hide', $pane->pid),
'attributes' => array('class' => array('use-ajax')),
);
}
else {
- $links[] = array(
+ $links['top']['enable'] = array(
'title' => t('Enable this pane'),
'href' => $this->get_url('show', $pane->pid),
'attributes' => array('class' => array('use-ajax')),
@@ -322,13 +322,13 @@ class panels_renderer_editor extends panels_renderer_standard {
}
if (isset($this->display->title_pane) && $this->display->title_pane == $pane->pid) {
- $links['panels-set-title'] = array(
+ $links['top']['panels-set-title'] = array(
'title' => t('✓Panel title'),
'html' => TRUE,
);
}
else {
- $links['panels-set-title'] = array(
+ $links['top']['panels-set-title'] = array(
'title' => t('Panel title'),
'href' => $this->get_url('panel-title', $pane->pid),
'attributes' => array('class' => array('use-ajax')),
@@ -338,7 +338,7 @@ class panels_renderer_editor extends panels_renderer_standard {
$subtype = ctools_content_get_subtype($content_type, $pane->subtype);
if (ctools_content_editable($content_type, $subtype, $pane->configuration)) {
- $links[] = array(
+ $links['top']['settings'] = array(
'title' => isset($content_type['edit text']) ? $content_type['edit text'] : t('Settings'),
'href' => $this->get_url('edit-pane', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
@@ -346,7 +346,7 @@ class panels_renderer_editor extends panels_renderer_standard {
}
if (user_access('administer advanced pane settings')) {
- $links[] = array(
+ $links['top']['css'] = array(
'title' => t('CSS properties'),
'href' => $this->get_url('pane-css', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
@@ -354,26 +354,10 @@ class panels_renderer_editor extends panels_renderer_standard {
}
if (user_access('administer panels styles')) {
- $links[] = array(
- 'title' => '
',
- 'html' => TRUE,
- );
-
- $style_links = $this->get_style_links('pane', $pane->pid);
-
- $links[] = array(
- 'title' => '' . t('Style') . '' . theme_links(array('links' => $style_links, 'attributes' => array(), 'heading' => array())),
- 'html' => TRUE,
- 'attributes' => array('class' => array('panels-sub-menu')),
- );
+ $links['style'] = $this->get_style_links('pane', $pane->pid);
}
if (user_access('administer pane access')) {
- $links[] = array(
- 'title' => '
',
- 'html' => TRUE,
- );
-
$contexts = $this->display->context;
// Make sure we have the logged in user context
if (!isset($contexts['logged-in-user'])) {
@@ -385,7 +369,7 @@ class panels_renderer_editor extends panels_renderer_standard {
if (!empty($pane->access['plugins'])) {
foreach ($pane->access['plugins'] as $id => $test) {
$plugin = ctools_get_access_plugin($test['name']);
- $access_title = isset($plugin['title']) ? $plugin['title'] : t('Broken/missing access plugin %plugin', array('%plugin' => $test['name']));
+ $access_title = isset($plugin['title']) ? $plugin['title'] : t('Broken/missing access plugin %plugin', array('%plugin' => $test['name']));
$access_description = ctools_access_summary($plugin, $contexts, $test);
$visibility_links[] = array(
@@ -396,37 +380,28 @@ class panels_renderer_editor extends panels_renderer_standard {
}
}
if (empty($visibility_links)) {
- $visibility_links[] = array(
+ $visibility_links['no_rules'] = array(
'title' => t('No rules'),
'attributes' => array('class' => array('panels-text')),
);
}
- $visibility_links[] = array(
+ $visibility_links['add_rule'] = array(
'title' => t('Add new rule'),
'href' => $this->get_url('access-add-test', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
);
- $visibility_links[] = array(
+ $visibility_links['settings'] = array(
'title' => t('Settings'),
'href' => $this->get_url('access-settings', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
);
- $links[] = array(
- 'title' => '' . t('Visibility rules') . '' . theme_links(array('links' => $visibility_links, 'attributes' => array(), 'heading' => array())),
- 'html' => TRUE,
- 'attributes' => array('class' => array('panels-sub-menu')),
- );
+ $links['visibility'] = $visibility_links;
}
if (user_access('use panels locks')) {
- $links[] = array(
- 'title' => '
',
- 'html' => TRUE,
- );
-
$lock_type = !empty($pane->locks['type']) ? $pane->locks['type'] : 'none';
switch ($lock_type) {
case 'immovable':
@@ -441,62 +416,44 @@ class panels_renderer_editor extends panels_renderer_standard {
break;
}
- $lock_links[] = array(
+ $lock_links['lock'] = array(
'title' => $lock_method,
'attributes' => array('class' => array('panels-text')),
);
- $lock_links[] = array(
+ $lock_links['change'] = array(
'title' => t('Change'),
'href' => $this->get_url('lock', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
);
- $links[] = array(
- 'title' => '' . t('Locking') . '' . theme_links(array('links' => $lock_links, 'attributes' => array(), 'heading' => array())),
- 'html' => TRUE,
- 'attributes' => array('class' => array('panels-sub-menu')),
- );
+ $links['lock'] = $lock_links;
}
if (panels_get_caches() && user_access('use panels caching features')) {
- $links[] = array(
- 'title' => '
',
- 'html' => TRUE,
- );
-
$method = isset($pane->cache['method']) ? $pane->cache['method'] : 0;
$info = panels_get_cache($method);
$cache_method = isset($info['title']) ? $info['title'] : t('No caching');
- $cache_links[] = array(
+ $cache_links['title'] = array(
'title' => $cache_method,
'attributes' => array('class' => array('panels-text')),
);
- $cache_links[] = array(
+ $cache_links['change'] = array(
'title' => t('Change'),
'href' => $this->get_url('cache-method', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
);
if (panels_plugin_get_function('cache', $info, 'settings form')) {
- $cache_links[] = array(
+ $cache_links['settings'] = array(
'title' => t('Settings'),
'href' => $this->get_url('cache-settings', $pane->pid),
'attributes' => array('class' => array('ctools-use-modal')),
);
}
- $links[] = array(
- 'title' => '' . t('Caching') . '' . theme_links(array('links' => $cache_links, 'attributes' => array(), 'heading' => array())),
- 'html' => TRUE,
- 'attributes' => array('class' => array('panels-sub-menu')),
- );
+ $links['cache'] = $cache_links;
}
- $links[] = array(
- 'title' => '
',
- 'html' => TRUE,
- );
-
- $links[] = array(
+ $links['bottom']['remove'] = array(
'title' => t('Remove'),
'href' => '#',
'attributes' => array(
@@ -505,7 +462,38 @@ class panels_renderer_editor extends panels_renderer_standard {
),
);
- return theme('ctools_dropdown', array('title' => theme('image', array('path' => ctools_image_path('icon-configure.png', 'panels'))), 'links' => $links, 'image' => TRUE));
+ // Allow others to add/remove links from pane context menu.
+ // Grouped by 'top', 'style', 'visibility', 'lock', 'cache' and 'bottom'
+ drupal_alter('get_pane_links', $links, $pane, $content_type);
+
+ $dropdown_links = $links['top'];
+ $category_labels = array(
+ 'style' => 'Style',
+ 'visibility' => 'Visibility rules',
+ 'lock' => 'Locking',
+ 'cache' => 'Caching',
+ );
+ foreach ($category_labels as $category => $label) {
+ if (array_key_exists($category, $links)) {
+ $dropdown_links[] = array(
+ 'title' => '
',
+ 'html' => TRUE,
+ );
+ $dropdown_links[] = array(
+ 'title' => '' . t($label) . '' . theme_links(array('links' => $links[$category], 'attributes' => array(), 'heading' => array())),
+ 'html' => TRUE,
+ 'attributes' => array('class' => array('panels-sub-menu')),
+ );
+ }
+ }
+
+ $dropdown_links[] = array(
+ 'title' => '
',
+ 'html' => TRUE,
+ );
+ $dropdown_links = array_merge($dropdown_links, $links['bottom']);
+
+ return theme('ctools_dropdown', array('title' => theme('image', array('path' => ctools_image_path('icon-configure.png', 'panels'))), 'links' => $dropdown_links, 'image' => TRUE));
}
// -----------------------------------------------------------------------
@@ -581,7 +569,7 @@ class panels_renderer_editor extends panels_renderer_standard {
* @todo -- this should be in CTools.
*/
function get_category($content_type) {
- if (isset($content_type['top level'])) {
+ if (!empty($content_type['top level'])) {
$category = 'root';
}
else if (isset($content_type['category'])) {
@@ -656,7 +644,19 @@ class panels_renderer_editor extends panels_renderer_standard {
$content_type = ctools_get_content_type($type_name);
$subtype = ctools_content_get_subtype($content_type, $subtype_name);
- if (!isset($step) || !isset($this->cache->new_pane)) {
+ // Determine if we are adding a different pane than previously cached. This
+ // is used to load the different pane into cache so that multistep forms
+ // have the correct context instead of a previously cached version that
+ // does not match the pane currently being added.
+ $is_different_pane = FALSE;
+ if (isset($this->cache) && isset($this->cache->new_pane)) {
+ $diff_type = $type_name != $this->cache->new_pane->type;
+ $diff_subtype = $subtype_name != $this->cache->new_pane->subtype;
+
+ $is_different_pane = $diff_type || $diff_subtype;
+ }
+
+ if (!isset($step) || !isset($this->cache->new_pane) || $is_different_pane) {
$pane = panels_new_pane($type_name, $subtype_name, TRUE);
$this->cache->new_pane = &$pane;
}
@@ -757,6 +757,11 @@ class panels_renderer_editor extends panels_renderer_standard {
// References get blown away with AJAX caching. This will fix that.
$this->cache->display->content[$pid] = $form_state['pane'];
+ // Conditionally overwrite the context for this panel if present in the form state.
+ if (!empty($form_state['display_cache']->display->context)) {
+ $this->cache->display->context = $form_state['display_cache']->display->context;
+ }
+
panels_edit_cache_set($this->cache);
$this->command_update_pane($pid);
$this->commands[] = ctools_modal_command_dismiss();
@@ -942,8 +947,11 @@ class panels_renderer_editor extends panels_renderer_standard {
ctools_include('content');
$pane = &$this->display->content[$pid];
$style = isset($pane->style['style']) ? $pane->style['style'] : 'default';
- $subtype = ctools_content_get_subtype($pane->type, $pane->subtype);
- $title = t('Pane style for "!pane"', array('!pane' => $subtype['title']));
+ $title = ctools_content_admin_title($pane->type, $pane->subtype, $pane->configuration, $this->display->context);
+ if (!$title) {
+ $title = $pane->type;
+ }
+ $title = t('Pane style for "!title"', array('!title' => $title));
break;
default:
@@ -1158,7 +1166,23 @@ class panels_renderer_editor extends panels_renderer_standard {
unset($this->cache->style);
}
- // $conf was a reference so it should just modify.
+ if (!empty($form_state['cancel'])) {
+ // The cache must be saved prior to dismissing the modal.
+ panels_edit_cache_set($this->cache);
+ $this->commands[] = ctools_modal_command_dismiss();
+ return;
+ }
+
+ // Copy settings from form state back into the cache.
+ if(!empty($form_state['values']['settings'])) {
+ if ($type == 'pane') {
+ $this->cache->display->content[$pid]->style['settings'] = $form_state['values']['settings'];
+ }
+ else if($type == 'region') {
+ $this->cache->display->panel_settings['style_settings'][$pid] = $form_state['values']['settings'];
+ }
+ }
+
panels_edit_cache_set($this->cache);
$this->commands[] = ctools_modal_command_dismiss();
@@ -1501,6 +1525,8 @@ function panels_ajax_edit_pane_cancel(&$form_state) {
* Choose cache method form
*/
function panels_edit_cache_method_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$conf = &$form_state['conf'];
@@ -1549,6 +1575,8 @@ function panels_edit_cache_method_form_submit($form, &$form_state) {
* Cache settings form
*/
function panels_edit_cache_settings_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$conf = &$form_state['conf'];
$pid = $form_state['pid'];
@@ -1609,6 +1637,8 @@ function panels_edit_cache_settings_form_submit($form, &$form_state) {
* Choose style form
*/
function panels_edit_style_type_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$style = $form_state['style'];
$type = $form_state['type'];
@@ -1659,6 +1689,8 @@ function panels_edit_style_type_form_submit($form, &$form_state) {
* Style settings form
*/
function panels_edit_style_settings_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$conf = &$form_state['conf'];
$pid = $form_state['pid'];
@@ -1683,9 +1715,28 @@ function panels_edit_style_settings_form($form, &$form_state) {
'#value' => t('Save'),
);
+
+ // Need a cancel button since the style cache can persist and impact the wrong
+ // pane (or region, or display).
+ $form['cancel_style'] = array(
+ '#type' => 'submit',
+ '#value' => t('Cancel'),
+ '#submit' => array('panels_edit_style_settings_form_cancel'),
+ );
+
return $form;
}
+/**
+ * Cancel style settings form.
+ *
+ * Clears the editing cache to prevent styles being applied to incorrect regions
+ * or panes.
+ */
+function panels_edit_style_settings_form_cancel($form, &$form_state) {
+ $form_state['cancel'] = TRUE;
+}
+
/**
* Validate style settings.
*/
@@ -1713,6 +1764,8 @@ function panels_edit_style_settings_form_submit($form, &$form_state) {
* Configure CSS on a pane form.
*/
function panels_edit_configure_pane_css_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$pane = &$form_state['pane'];
@@ -1755,6 +1808,8 @@ function panels_edit_configure_pane_css_form_submit($form, &$form_state) {
* Configure lock on a pane form.
*/
function panels_edit_configure_pane_lock_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$pane = &$form_state['pane'];
@@ -1830,6 +1885,8 @@ function panels_edit_configure_pane_lock_form_submit($form, &$form_state) {
* Form to control basic visibility settings.
*/
function panels_edit_configure_access_settings_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$pane = &$form_state['pane'];
@@ -1867,6 +1924,8 @@ function panels_edit_configure_access_settings_form_submit($form, &$form_state)
* Form to add a visibility rule.
*/
function panels_edit_add_access_test_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$pane = &$form_state['pane'];
@@ -1896,6 +1955,8 @@ function panels_edit_add_access_test_form($form, &$form_state) {
* Form to configure a visibility rule.
*/
function panels_edit_configure_access_test_form($form, &$form_state) {
+ ctools_form_include($form_state, 'plugins', 'panels');
+ form_load_include($form_state, 'php', 'panels', '/plugins/display_renderers/panels_renderer_editor.class');
$display = &$form_state['display'];
$test = &$form_state['test'];
$plugin = &$form_state['plugin'];
diff --git a/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_standard.class.php b/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_standard.class.php
index c2046730..1de5bf41 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_standard.class.php
+++ b/sites/all/modules/contrib/panels/panels/plugins/display_renderers/panels_renderer_standard.class.php
@@ -259,6 +259,10 @@ class panels_renderer_standard {
}
}
$this->prepared['panes'] = $first + $normal + $last;
+
+ // Allow other modules the alter the prepared panes array.
+ drupal_alter('panels_panes_prepared', $this->prepared['panes'], $this);
+
return $this->prepared['panes'];
}
@@ -403,17 +407,38 @@ class panels_renderer_standard {
* their CSS added in the right order: inner content before outer content.
*/
function add_meta() {
+ global $theme;
+
if (!empty($this->plugins['layout']['css'])) {
- if (file_exists(path_to_theme() . '/' . $this->plugins['layout']['css'])) {
- $this->add_css(path_to_theme() . '/' . $this->plugins['layout']['css']);
+ // Do not use the path_to_theme() function, because it returns the
+ // $GLOBALS['theme_path'] value, which may be overriden in the theme()
+ // function when the theme hook defines the key 'theme path'.
+ $theme_path = isset($theme) ? drupal_get_path('theme', $theme) : '';
+
+ $css = $this->plugins['layout']['css'];
+ if (!is_array($css)) {
+ $css = array($css);
}
- else {
- $this->add_css($this->plugins['layout']['path'] . '/' . $this->plugins['layout']['css']);
+
+ // Load each of the CSS files defined in this layout.
+ foreach ($css as $file) {
+ if (!empty($theme_path) && file_exists($theme_path . '/' . $file)) {
+ $this->add_css($theme_path . '/' . $file);
+ }
+ else {
+ $this->add_css($this->plugins['layout']['path'] . '/' . $file);
+ }
}
}
if ($this->admin && isset($this->plugins['layout']['admin css'])) {
- $this->add_css($this->plugins['layout']['path'] . '/' . $this->plugins['layout']['admin css']);
+ $admin_css = $this->plugins['layout']['admin css'];
+ if (!is_array($admin_css)) {
+ $admin_css = array($admin_css);
+ }
+ foreach ($admin_css as $file) {
+ $this->add_css($this->plugins['layout']['path'] . '/' . $file);
+ }
}
}
@@ -436,7 +461,7 @@ class panels_renderer_standard {
break;
case 'inline':
$url = base_path() . $filename;
- $this->prefix .= ''."\n";
+ $this->prefix .= ''."\n";
break;
}
}
@@ -473,6 +498,8 @@ class panels_renderer_standard {
* A Panels pane object, as loaded from the database.
*/
function render_pane(&$pane) {
+ module_invoke_all('panels_pane_prerender', $pane);
+
$content = $this->render_pane_content($pane);
if ($this->display->hide_title == PANELS_TITLE_PANE && !empty($this->display->title_pane) && $this->display->title_pane == $pane->pid) {
@@ -526,7 +553,6 @@ class panels_renderer_standard {
$this->display->context = array();
}
- $content = FALSE;
$caching = !empty($pane->cache['method']) && empty($this->display->skip_cache);
if ($caching && ($cache = panels_get_cached_content($this->display, $this->display->args, $this->display->context, $pane))) {
$content = $cache->content;
@@ -540,10 +566,6 @@ class panels_renderer_standard {
$content = ctools_content_render($pane->type, $pane->subtype, $pane->configuration, array(), $this->display->args, $this->display->context);
- if (empty($content)) {
- return;
- }
-
foreach (module_implements('panels_pane_content_alter') as $module) {
$function = $module . '_panels_pane_content_alter';
$function($content, $pane, $this->display->args, $this->display->context, $this, $this->display);
@@ -555,14 +577,17 @@ class panels_renderer_standard {
}
}
- // Pass long the css_id that is usually available.
- if (!empty($pane->css['css_id'])) {
- $content->css_id = check_plain($pane->css['css_id']);
- }
+ // If there's content, check if we've css configuration to add.
+ if (!empty($content)) {
+ // Pass long the css_id that is usually available.
+ if (!empty($pane->css['css_id'])) {
+ $content->css_id = check_plain($pane->css['css_id']);
+ }
- // Pass long the css_class that is usually available.
- if (!empty($pane->css['css_class'])) {
- $content->css_class = check_plain($pane->css['css_class']);
+ // Pass long the css_class that is usually available.
+ if (!empty($pane->css['css_class'])) {
+ $content->css_class = check_plain($pane->css['css_class']);
+ }
}
return $content;
diff --git a/sites/all/modules/contrib/panels/panels/plugins/export_ui/panels_layouts_ui.class.php b/sites/all/modules/contrib/panels/panels/plugins/export_ui/panels_layouts_ui.class.php
index 256cdaa9..ecf3b7bd 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/export_ui/panels_layouts_ui.class.php
+++ b/sites/all/modules/contrib/panels/panels/plugins/export_ui/panels_layouts_ui.class.php
@@ -125,9 +125,19 @@ class panels_layouts_ui extends ctools_export_ui {
function edit_form_submit(&$form, &$form_state) {
parent::edit_form_submit($form, $form_state);
+
+ // While we short circuited the main submit hook, we need to keep this one.
+ panels_edit_display_settings_form_submit($form, $form_state);
$form_state['item']->settings = $form_state['display']->layout_settings;
}
+ function edit_form_validate(&$form, &$form_state) {
+ parent::edit_form_validate($form, $form_state);
+
+ // While we short circuited the main validate hook, we need to keep this one.
+ panels_edit_display_settings_form_validate($form, $form_state);
+ }
+
function list_form(&$form, &$form_state) {
ctools_include('plugins', 'panels');
$this->builders = panels_get_layout_builders();
diff --git a/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible-admin.js b/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible-admin.js
index 10c6dd09..2cddd42d 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible-admin.js
+++ b/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible-admin.js
@@ -17,7 +17,7 @@ Drupal.flexible.fixHeight = function() {
Drupal.behaviors.flexibleAdmin = {
attach: function(context) {
// Show/hide layout manager button
- $('input#panels-flexible-toggle-layout:not(.panels-flexible-processed)', context)
+ $('#panels-flexible-toggle-layout:not(.panels-flexible-processed)', context)
.addClass('panels-flexible-processed')
.click(function() {
$('.panel-flexible-admin')
@@ -290,9 +290,6 @@ Drupal.flexible.splitter = function($splitter) {
// if not moving the right side, adjust the parent padding instead.
splitter.parent.css('padding-left', (splitter.left_padding - moved) + 'px');
splitter.left.parent().css('margin-left', (splitter.left_parent + moved) + 'px');
- if (jQuery.browser.msie) {
- splitter.left.parent().css('left', splitter.currentLeft);
- }
}
return false;
};
diff --git a/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible.inc b/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible.inc
index 93a7c93c..4cdb216b 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible.inc
+++ b/sites/all/modules/contrib/panels/panels/plugins/layouts/flexible/flexible.inc
@@ -471,15 +471,26 @@ function panels_flexible_render_items($renderer, $list, $owner_id) {
switch ($item['type']) {
case 'column':
$content = panels_flexible_render_items($renderer, $item['children'], $renderer->base['column'] . '-' . $id);
- $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
+ if (empty($renderer->settings['items'][$id]['hide_empty']) || trim($content)) {
+ $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
+ }
break;
case 'row':
$content = panels_flexible_render_items($renderer, $item['children'], $renderer->base['row'] . '-' . $id);
- $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max, TRUE);
+ if (empty($renderer->settings['items'][$id]['hide_empty']) || trim($content)) {
+ $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max, TRUE);
+ }
break;
case 'region':
- $content = isset($renderer->content[$id]) ? $renderer->content[$id] : " ";
- $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
+ if (empty($renderer->settings['items'][$id]['hide_empty'])) {
+ $content = isset($renderer->content[$id]) ? $renderer->content[$id] : " ";
+ }
+ else {
+ $content = isset($renderer->content[$id]) ? trim($renderer->content[$id]) : "";
+ }
+ if (empty($renderer->settings['items'][$id]['hide_empty']) || $content) {
+ $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
+ }
break;
}
@@ -747,47 +758,47 @@ function panels_flexible_render_css_group($renderer, $list, $owner_id, $type, $i
$css = array();
// Start off with some generic CSS to properly pad regions
- $css['.' . $renderer->item_class['region']] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['region']] = array(
'padding' => '0',
);
- $css['.' . $renderer->item_class['region'] . '-inside'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['region'] . '-inside'] = array(
'padding-right' => $renderer->region_separation,
'padding-left' => $renderer->region_separation,
);
- $css['.' . $renderer->item_class['region'] . '-inside-first'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['region'] . '-inside-first'] = array(
'padding-left' => '0',
);
- $css['.' . $renderer->item_class['region'] . '-inside-last'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['region'] . '-inside-last'] = array(
'padding-right' => '0',
);
- $css['.' . $renderer->item_class['column']] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['column']] = array(
'padding' => '0',
);
- $css['.' . $renderer->item_class['column'] . '-inside'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['column'] . '-inside'] = array(
'padding-right' => $renderer->column_separation,
'padding-left' => $renderer->column_separation,
);
- $css['.' . $renderer->item_class['column'] . '-inside-first'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['column'] . '-inside-first'] = array(
'padding-left' => '0',
);
- $css['.' . $renderer->item_class['column'] . '-inside-last'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['column'] . '-inside-last'] = array(
'padding-right' => '0',
);
// And properly pad rows too
- $css['.' . $renderer->item_class['row']] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['row']] = array(
'padding' => '0 0 ' . $renderer->row_separation . ' 0',
'margin' => '0',
);
- $css['.' . $renderer->item_class['row'] . '-last'] = array(
+ $css[$owner_id . ' .' . $renderer->item_class['row'] . '-last'] = array(
'padding-bottom' => '0',
);
@@ -1186,6 +1197,12 @@ function panels_flexible_config_item_form($form, &$form_state) {
}
}
+ $form['hide_empty'] = array(
+ '#title' => t('Hide element if empty'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($item['hide_empty']) ? 1 : 0,
+ );
+
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
@@ -1223,6 +1240,7 @@ function panels_flexible_config_item_form_submit(&$form, &$form_state) {
else {
$item['contains'] = $form_state['values']['contains'];
}
+ $item['hide_empty'] = $form_state['values']['hide_empty'];
}
@@ -1486,6 +1504,12 @@ function panels_flexible_add_item_form($form, &$form_state) {
);
}
+ $form['hide_empty'] = array(
+ '#title' => t('Hide element if empty'),
+ '#type' => 'checkbox',
+ '#default_value' => 0,
+ );
+
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
@@ -1516,6 +1540,8 @@ function panels_flexible_add_item_form_submit(&$form, &$form_state) {
$item['contains'] = $form_state['values']['contains'];
}
+ $item['hide_empty'] = $form_state['values']['hide_empty'];
+
if ($item['type'] == 'region') {
// derive the region key from the title
$key = preg_replace("/[^a-z0-9]/", '_', drupal_strtolower($item['title']));
diff --git a/sites/all/modules/contrib/panels/panels/plugins/layouts/threecol_25_50_25_stacked/threecol_25_50_25_stacked.css b/sites/all/modules/contrib/panels/panels/plugins/layouts/threecol_25_50_25_stacked/threecol_25_50_25_stacked.css
index 1aa00dad..c24dabf9 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/layouts/threecol_25_50_25_stacked/threecol_25_50_25_stacked.css
+++ b/sites/all/modules/contrib/panels/panels/plugins/layouts/threecol_25_50_25_stacked/threecol_25_50_25_stacked.css
@@ -18,7 +18,7 @@
width: 25%;
}
-.panel-3col-stacked .panel-col .inside {
+.panel-3col-stacked .panel-col-first .inside {
margin: 0 .5em 1em .5em;
}
diff --git a/sites/all/modules/contrib/panels/panels/plugins/styles/block.inc b/sites/all/modules/contrib/panels/panels/plugins/styles/block.inc
index 4614ee49..58815a32 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/styles/block.inc
+++ b/sites/all/modules/contrib/panels/panels/plugins/styles/block.inc
@@ -31,6 +31,9 @@ function theme_panels_block_style_render_pane($vars) {
if (!empty($block->title)) {
$block->subject = $block->title;
}
+ if (!isset($block->subject)) {
+ $block->subject = '';
+ }
$block->region = $pane->panel;
if (!isset($block->module)) {
diff --git a/sites/all/modules/contrib/panels/panels/plugins/task_handlers/panel_context.inc b/sites/all/modules/contrib/panels/panels/plugins/task_handlers/panel_context.inc
index ba4ae9fa..9f03b554 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/task_handlers/panel_context.inc
+++ b/sites/all/modules/contrib/panels/panels/plugins/task_handlers/panel_context.inc
@@ -184,7 +184,7 @@ $plugin = array(
'default conf' => array(
'title' => t('Panel'),
'no_blocks' => FALSE,
- 'pipeline' => 'standard',
+ 'pipeline' => variable_get('panels_renderer_default', 'standard'),
'body_classes_to_remove' => '',
'body_classes_to_add' => '',
'css_id' => '',
@@ -238,6 +238,28 @@ function &panels_panel_context_get_display(&$handler) {
return $handler->conf['display'];
}
+/**
+ * Build the cache key so that the editor and IPE can properly find
+ * everything needed for this display.
+ */
+function panels_panel_context_cache_key($task_name, $handler_id, $args) {
+ $arguments = array();
+ foreach ($args as $arg) {
+ // Sadly things like panels everywhere actually use non-string arguments
+ // and they basically can't be represented here. Luckily, PE also does
+ // not use a system where this matters, so replace its args with a 0
+ // for a placeholder.
+ if (is_string($arg)) {
+ $arguments[] = $arg;
+ }
+ else {
+ $arguments[] = '0';
+ }
+ }
+ $cache_key = 'panel_context:' . $task_name . '::' . $handler_id . '::' . implode('\\', $arguments) . '::';
+ return $cache_key;
+}
+
/**
* Check selection rules and, if passed, render the contexts.
*/
@@ -267,7 +289,7 @@ function panels_panel_context_render($handler, $base_contexts, $args, $test = TR
$display->css_id = $handler->conf['css_id'];
$task_name = page_manager_make_task_name($handler->task, $handler->subtask);
- $display->cache_key = 'panel_context:' . $task_name . ':' . $handler->name;
+ $display->cache_key = panels_panel_context_cache_key($task_name, $handler->name, $args);
// Check to see if there is any CSS.
if (!empty($handler->conf['css'])) {
@@ -284,15 +306,32 @@ function panels_panel_context_render($handler, $base_contexts, $args, $test = TR
panels_get_current_page_display($display);
$renderer = panels_get_renderer($handler->conf['pipeline'], $display);
+ // If the IPE is enabled, but the user does not have access to edit
+ // load the standard renderer instead.
+
+ $parents = class_parents($renderer);
+ if (!empty($parents['panels_renderer_editor']) && !user_access('user page manager') && !user_access('use ipe with page manager')) {
+ $renderer = panels_get_renderer_handler('standard', $display);
+ }
// Remove and add body element classes
$panel_body_css = &drupal_static('panel_body_css');
- if (isset($handler->conf['body_classes_to_remove']) && !isset($panel_body_css['body_classes_to_remove'])) {
- $panel_body_css['body_classes_to_remove'] = $handler->conf['body_classes_to_remove'];
+ if (isset($handler->conf['body_classes_to_remove'])) {
+ if (!isset($panel_body_css['body_classes_to_remove'])) {
+ $panel_body_css['body_classes_to_remove'] = $handler->conf['body_classes_to_remove'];
+ }
+ else{
+ $panel_body_css['body_classes_to_remove'] .= ' ' . $handler->conf['body_classes_to_remove'];
+ }
}
- if (isset($handler->conf['body_classes_to_add']) && !isset($panel_body_css['body_classes_to_add'])) {
- $panel_body_css['body_classes_to_add'] = $handler->conf['body_classes_to_add'];
+ if (isset($handler->conf['body_classes_to_add'])) {
+ if (!isset($panel_body_css['body_classes_to_add'])) {
+ $panel_body_css['body_classes_to_add'] = $handler->conf['body_classes_to_add'];
+ }
+ else {
+ $panel_body_css['body_classes_to_add'] .= ' '. $handler->conf['body_classes_to_add'];
+ }
}
$info = array(
@@ -364,7 +403,10 @@ function panels_panel_context_export(&$handler, $indent) {
unset($handler->conf[$item]);
}
}
- $display->did = 'new';
+ $display = (object) array(
+ 'did' => 'new',
+ 'uuid' => ctools_uuid_generate(),
+ );
$handler->conf['display'] = $display;
}
@@ -629,7 +671,7 @@ function panels_panel_context_edit_move($form, &$form_state) {
$form_state['display'] = &panels_panel_context_get_display($form_state['handler']);
$form_state['layout'] = $form_state['handler']->conf['temp_layout'];
- $form_state['cache_key'] = 'panel_context:' . $form_state['task_name'] . ':' . $form_state['handler_id'];
+ $form_state['cache_key'] = panels_panel_context_cache_key($form_state['task_name'], $form_state['handler_id'], array());
ctools_include('common', 'panels');
ctools_include('display-layout', 'panels');
@@ -666,7 +708,7 @@ function panels_panel_context_edit_content($form, &$form_state) {
ctools_include('context');
ctools_include('context-task-handler');
- $cache = panels_edit_cache_get('panel_context:' . $form_state['task_name'] . ':' . $form_state['handler_id']);
+ $cache = panels_edit_cache_get(panels_panel_context_cache_key($form_state['task_name'], $form_state['handler_id'], array()));
$form_state['renderer'] = panels_get_renderer_handler('editor', $cache->display);
$form_state['renderer']->cache = &$cache;
@@ -731,7 +773,7 @@ function panels_panel_context_edit_settings($form, &$form_state) {
$form['conf']['body_classes_to_remove'] = array(
'#type' => 'textfield',
'#size' => 128,
- '#default_value' => $conf['body_classes_to_remove'],
+ '#default_value' => empty($conf['body_classes_to_remove']) ? '' : $conf['body_classes_to_remove'],
'#title' => t('Remove body CSS classes'),
'#description' => t('The CSS classes to remove from the body element of this page. Separated by a space. For example: no-sidebars one-sidebar sidebar-first sidebar-second two-sidebars.'),
);
@@ -739,7 +781,7 @@ function panels_panel_context_edit_settings($form, &$form_state) {
$form['conf']['body_classes_to_add'] = array(
'#type' => 'textfield',
'#size' => 128,
- '#default_value' => $conf['body_classes_to_add'],
+ '#default_value' => empty($conf['body_classes_to_add']) ? '' : $conf['body_classes_to_add'],
'#title' => t('Add body CSS classes'),
'#description' => t('The CSS classes to add to the body element of this page. Separated by a space. For example: no-sidebars one-sidebar sidebar-first sidebar-second two-sidebars.'),
);
@@ -868,15 +910,28 @@ function panels_panel_context_get_addressable($task, $subtask_name, $handler, $a
$display->context = $contexts;
$display->args = $arguments;
$display->css_id = $handler->conf['css_id'];
- $display->cache_key = 'panel_context:' . $task->name . ':' . $handler->name;
+ $display->cache_key = panels_panel_context_cache_key($task->name, $handler->name, $arguments);
$renderer = panels_get_renderer($handler->conf['pipeline'], $display);
- if ($type == 'content') {
- $renderer->prepare();
+ $renderer->prepare();
+ if ($address) {
$pid = array_shift($address);
if (!empty($renderer->prepared['panes'][$pid])) {
- return $renderer->render_pane($renderer->prepared['panes'][$pid]);
+ if ($type == 'content') {
+ return $renderer->render_pane($renderer->prepared['panes'][$pid]);
+ }
+ elseif ($type == 'pane') {
+ return $renderer->prepared['panes'][$pid];
+ }
+ }
+ }
+ else {
+ if ($type == 'content') {
+ return $renderer->render();
+ }
+ elseif ($type == 'renderer') {
+ return $renderer;
}
}
}
diff --git a/sites/all/modules/contrib/panels/panels/plugins/views/panels_views_plugin_row_fields.inc b/sites/all/modules/contrib/panels/panels/plugins/views/panels_views_plugin_row_fields.inc
index 814350e4..4a404abc 100644
--- a/sites/all/modules/contrib/panels/panels/plugins/views/panels_views_plugin_row_fields.inc
+++ b/sites/all/modules/contrib/panels/panels/plugins/views/panels_views_plugin_row_fields.inc
@@ -137,8 +137,8 @@ class panels_views_plugin_row_fields extends views_plugin_row_fields {
// Now that we have distributed our fields, go through the regions and
// render them into the content array.
- foreach ($this->region_fields as $region_id => $fields) {
- $this->view->field = $fields;
+ foreach ($this->region_fields as $region_id => $fields_list) {
+ $this->view->field = $fields_list;
$content[$region_id] = theme($this->theme_functions(), array('view' => $this->view, 'options' => $this->options, 'row' => $row));
}
diff --git a/sites/all/modules/contrib/panels/panels/templates/panels-add-content-link.tpl.php b/sites/all/modules/contrib/panels/panels/templates/panels-add-content-link.tpl.php
index 3af6dbcb..149ac829 100644
--- a/sites/all/modules/contrib/panels/panels/templates/panels-add-content-link.tpl.php
+++ b/sites/all/modules/contrib/panels/panels/templates/panels-add-content-link.tpl.php
@@ -1,10 +1,14 @@
-
-
+
+
diff --git a/sites/all/modules/contrib/panels/panels/templates/panels-pane.tpl.php b/sites/all/modules/contrib/panels/panels/templates/panels-pane.tpl.php
index 93dd113d..0aa01021 100644
--- a/sites/all/modules/contrib/panels/panels/templates/panels-pane.tpl.php
+++ b/sites/all/modules/contrib/panels/panels/templates/panels-pane.tpl.php
@@ -20,14 +20,16 @@
->
+ >
- >
+ <>
+
+ >
diff --git a/sites/all/modules/contrib/rss/feed_path_publisher/feed_path_publisher.info b/sites/all/modules/contrib/rss/feed_path_publisher/feed_path_publisher.info
index 5944dcc2..efb6480a 100644
--- a/sites/all/modules/contrib/rss/feed_path_publisher/feed_path_publisher.info
+++ b/sites/all/modules/contrib/rss/feed_path_publisher/feed_path_publisher.info
@@ -1,10 +1,11 @@
name = Feed Path Publisher
description = Publishes RSS feeds sitewide and to specific paths.
core = 7.x
+configure = admin/config/content/feed_path_publisher
-; Information added by drupal.org packaging script on 2013-03-19
-version = "7.x-1.0-beta1+1-dev"
+; Information added by Drupal.org packaging script on 2014-01-09
+version = "7.x-1.0-beta1+2-dev"
core = "7.x"
project = "feed_path_publisher"
-datestamp = "1363654954"
+datestamp = "1389285506"
diff --git a/sites/all/modules/contrib/seo/metatag/CHANGELOG.txt b/sites/all/modules/contrib/seo/metatag/CHANGELOG.txt
index b7d67f7f..adaec00a 100644
--- a/sites/all/modules/contrib/seo/metatag/CHANGELOG.txt
+++ b/sites/all/modules/contrib/seo/metatag/CHANGELOG.txt
@@ -1,5 +1,178 @@
-Metatag 7.x-1.x-dev, xxxx-xx-xx
+Metatag 7.x-1.4, 2014-10-09
+---------------------------
+#2353079 by DamienMcKenna: Fixed Views integration, for real this time.
+#2344877 by DamienMcKenna: Fixed Panels integration, for real this time.
+
+
+Metatag 7.x-1.3, 2014-10-07
+---------------------------
+#2350967 by das-peter, DamienMcKenna: Fatal error occurred loading any View that
+ did not have meta tags assigned.
+#2344877 by DamienMcKenna, Mau Palantír, libelle2000: Fixed Panels integration.
+By DamienMcKenna: metatag_metatags_load()'s documentation was incorrect.
+#2347193 by DamienMcKenna: Updated Feeds integration to be compatible with the
+ new data structures in 1.0, and revision_id problems.
+
+
+Metatag 7.x-1.2, 2014-10-04
+---------------------------
+#2343909 by DamienMcKenna: Unable to update meta tags on nodes that didn't
+ contain translations.
+#2185791 by DamienMcKenna: Improved logic for deciding which meta tag values
+ to use for the current language; new advanced option allows loading of the
+ entity's default language's values if nothing else matches.
+#2346159 by DamienMcKenna: Fixed tag dependencies, which were broken in 1.0.
+#2346153 by DamienMcKenna: Added Twitter app 'name' tags, misc improvements to
+ Twitter Cards code.
+#2185791 by DamienMcKenna: Changed the no-values-to-load entity language default
+ logic so that the default language values will be loaded unless disabled.
+#1304038 by DamienMcKenna: Indicate in the README.txt how to disable output for
+ the three meta tags output by Drupal core by default.
+#2350129 by DamienMcKenna: Added a Drush command for clearing Metatag's caches.
+#2341795 by DamienMcKenna: Updated Metatag:Views to be compatible with the new
+ form data structure in 1.0.
+#2292043 by eric.chenchao, DamienMcKenna: Added Google+ 'itemprop' meta tags.
+#2341795 by DamienMcKenna: Fixed Views previews.
+#2289139 by maijs, DamienMcKenna: Allow each Views display to have different
+ meta tag values.
+
+
+Metatag 7.x-1.1, 2014-09-18
+---------------------------
+#2340639 by agoradesign: Additional check needed in hook_requirements to avoid
+ breaking installation profiles.
+#2340337 by DamienMcKenna: Config system updated for the new language-based
+ data handling.
+#2330823 by DamienMcKenna: REVERT: Remove the deprecated G+ Author meta tag.
+
+
+Metatag 7.x-1.0, 2014-09-17
+---------------------------
+#2319389 by DamienMcKenna: Additional Open Graph meta tags, for videos.
+#2169575 by gvorbeck: Workbench Moderation v2 doesn't need any hackery, so
+ removed the message in hook_requirements().
+#2140189 by ttkaminski, DamienMcKenna: Added an index to {metatag} table for the
+ 'type' and 'revision_id' fields.
+#1391554 by DamienMcKenna: Handle scenarios where the legacy "metatags" module
+ had been installed.
+#2325459 by DamienMcKenna: Used JSHint to correct some minor JS bugs.
+#2326197 by Dave Reid: metatag_generate_entity_metatags() cache can be bypassed.
+By DamienMcKenna: Updated the og:image size guidelines.
+By DamienMcKenna: Remove the redundant metatag_taxonomy_term_view_alter().
+By DamienMcKenna: Support Twitter Cards fieldset in Metatag:Context.
+#1778286 by alberto56: Removed the deprecated metatag_ui module.
+#2331677 by othermachines: Updates 7025 and 7027 attempted to update the wrong
+ tables.
+#2330823 by othermachines: Remove the deprecated G+ Author meta tag.
+#2186155 by DamienMcKenna, grahamC, JeroenT: Resolved problems when saving an
+ entity directly rather than via entity form.
+By DamienMcKenna: Corrected the namespace prefix for OG video meta tags.
+#2186155 by DamienMcKenna: Follow-up to fix a number of scenarios.
+
+
+Metatag 7.x-1.0-rc2, 2014-08-05
-------------------------------
+#1904266 by mvwensen, DamienMcKenna: Added the dcterms.modified meta tag.
+#2202031 by DamienMcKenna: Don't double-encode output, handle specially.
+#2026343 by DamienMcKenna, skruf, valkum, wxman: Added many more Open Graph meta
+ tags.
+#2164919 by DamienMcKenna: Added an Advanced Settings page.
+#2241083 by DamienMcKenna: API structure for definiting field dependencies;
+ currently limited to hiding/showing fields, can be expanded later with
+ validation logic. Initial implementation for some Open Graph and Twitter Cards
+ meta tags.
+#2307523 by leewillis77, DamienMcKenna: Additional arguments for two
+ drupal_alter hooks.
+#2241083 by rooby: Refactored meta tag output generation using a new function,
+ metatag_generate_entity_metatags(), allowing for the tags to be independently
+ obtained for a given entity.
+#2262159 by DamienMcKenna: Bumped core requirement to 7.28, removed the
+ [node:summary] fix that's no longer needed.
+#2306449 by DamienMcKenna: Not having the Transliteration or Imagecache Token
+ modules installed no longer reports an error in hook_requirements().
+#1328562 by andremolnar, Greg Boggs, DamienMcKenna: Improved form descriptions.
+#1918706 by theunraveler, DamienMcKenna, Zekvyrin, JeroenT: [current-page:title]
+ didn't work correctly on Panels pages.
+#2153977 by paolomainardi, DamienMcKenna: Fix for translations of base entity
+ type configuration when there is no bundle configuration.
+
+
+Metatag 7.x-1.0-rc1, 2014-07-12
+-------------------------------
+By DamienMcKenna: Small improvement to the comment on update 7007.
+#2196393 by generalconsensus, aprohl5: Typo in hook_install().
+#2237507 by SebCorbin: Only delete all records when editing one entity revision.
+#2056739 by B-Prod: Incorrect language handling when displaying entity pages
+ using Panels.
+#2205675 by Romlam, greggles: Typo in variable name caused data to not load.
+#2265447 by opdavies: Ignore comment entities, conflict with comment_fragment.
+#2271685 by adee147: Typos in metatag_metatags_cache_clear().
+#2271811 by DamienMcKenna: Replaced theme_metatag_opengraph() with
+ theme_metatag_property().
+#1282636 by DamienMcKenna: Support meta tags that allow multiple values; first
+ supported tags are og:image and og:image:secure_url.
+#2273459 by DamienMcKenna: Improved Twitter Cards default values.
+#2273241 by DamienMcKenna: Use the new hook_metatag_bundled_config_alter() to
+ load settings from submodules.
+#2273493 by DamienMcKenna: Improved Dublin Core default values.
+#2274921 by DamienMcKenna: Token browser link missing on settings pages.
+#2277787 by eugene.ilyin: Missing translations in metatag_context.
+By DamienMcKenna: Removed duplicate 'devel_generate' setting for 'image_src'.
+#2282903 by DamienMcKenna: Special handling for meta tags that need to output a
+ secure URL, replace 'http://' with 'https://'.
+#2281833 by DamienMcKenna: Ensure multi-item values are output in a consistent
+ order.
+#2275323 by drastik: Provide link to settings page in Metatag:Context module.
+#1284810 by DamienMcKenna: Really recommend installing Imagecache Token.
+#1809356 by DamienMcKenna: Sort all meta tags.
+#2276361 by DamienMcKenna: Move Facebook meta tags into a separate submodule.
+#2185943 by fizk: Remove warnings about Exclude Node Title.
+#2266595 by hefox: Change watchdog() message to a warning not critical, to avoid
+ problems with Jenkins.
+#2193195 by 75th Trombone: Corrected a variable usage in README.txt.
+#1338612 by Lasac, DamienMcKenna: Added the content-language meta tag.
+#2291993 by DamienMcKenna: Duplicate fb meta tags causes lots of errors.
+#2285787 by SebCorbin: Entity Translation problems with revisions.
+#2025425 by moonray, David_Rothstein, hefox, DamienMcKenna: Cache improvement
+ to separate entity vs page language.
+#2186241 by nnevill.io1, DamienMcKenna: Revisions support for Panels.
+#2051407 by cha0s, DamienMcKenna: Language support for token integration.
+#2183203 by mikeytown2, juampy, DamienMcKenna: Improved queries in
+ metatag_metatags_load_multiple().
+#2227377 by DamienMcKenna: taxonomy_vocabulary_load() caused problems when
+ executed during hook_entity_info_alter().
+#1995564 by DamienMcKenna, willieseabrook: Added a warning about a possible
+ conflict with the Admin Language module.
+#2298337 by DamienMcKenna: Added an API option to indicate one meta tag replaces
+ another; updated API docs accordingly.
+#2267501 by DamienMcKenna: Renamed the 'twitter:image' meta tag to the correct
+ 'twitter:image:src'.
+#2121437 by DamienMcKenna: Renamed the 'copyright' meta tag to the correct
+ 'rights' tag.
+#2177455 by DamienMcKenna: Avoid errors when updating from older releases due
+ to missing revision_id field.
+#2178411 by DamienMcKenna, kporras07: Language not assigned correctly on CTools
+ -based pages.
+
+
+Metatag 7.x-1.0-beta9, 2014-01-18
+---------------------------------
+#2174363 by DamienMcKenna: Changed update 7018 to avoid attempting to create
+ duplicate records when updating; instead should there be a collision the
+ record with revision_id 0 will be deleted.
+#2176351 by DamienMcKenna: 403 and 404 error pages will use the global default
+ for the page title instead of copying the homepage's.
+#2175843 by DamienMcKenna: It was possible to get to update 7016 without the
+ revision_id field existing, so make sure it exists.
+#2081787 by attila.fekete: Don't let Metatag:Views overwrite the frontpage meta
+ tag config, matching how Metatag:Panels works.
+#2176375 by DamienMcKenna: Added note to README.txt about the Textimage module's
+ compatibility with Metatag.
+#2170771 by DamienMcKenna: Added support for the og:image:secure_url meta tag.
+
+
+Metatag 7.x-1.0-beta8, 2014-01-15
+---------------------------------
#1995284 by DamienMcKenna: Replace $_SERVER['REQUEST_URI'] with request_uri().
By DamienMcKenna: Updated the README.txt's Credits section to match the project
page.
@@ -56,6 +229,59 @@ By DamienMcKenna: Moved hook_requirements to the top of metatag.install.
#2095397 by DamienMcKenna: Allow method to skip skipping metatag_entity_view().
#2095501 by DamienMcKenna: Logic mistake in metatag_metatags_delete_multiple()
meant records were never deleted.
+#2072087 by brunascle: Twitter Cards changed to use correct 'name' attribute.
+#2086037 by greggles: Only show schema warning messages to appropriate people.
+#1311050 by pasive, DamienMcKenna: Added the og:locale meta tag.
+#2082539 by DamienMcKenna, hswong3i: {metatag}.revision_id cannot be null.
+#2082539 by DamienMcKenna: Follow-up to make all revision_id values numeric.
+#1848338 by DamienMcKenna: Added a list of test scenarios that need to be added.
+#2152043 by DamienMcKenna: Devel Generate integration via Metatag:Devel
+ submodule.
+#2152043 by DamienMcKenna: Expanded Devel Generate integration to cover almost
+ all included meta tags.
+#1572474 by DamienMcKenna, HyperGlide, jyee, Kristen Pol, sylus: Fixes for
+ revisions support.
+#1876042 by DamienMcKenna: Rename variables to use $entity_id instead of $id
+ in metatag.admin.inc, $entity_type instead of $type in metatag.migrate.inc.
+#2157689/#2088299 by travelertt, iMiksu, DamienMcKenna: JS error broke
+ CKEditor, etc.
+#2168343 by DamienMcKenna: Clear EntityCache bins.
+#2062379 by DamienMcKenna: Restructured caching.
+#2168939 by DamienMcKenna: Don't skip batch processing on updates ran via Drush.
+#2169547 by DamienMcKenna: Clarification on Workbench Moderation support.
+#2090557 by Kristen Pol, DamienMcKenna: Don't cache tags on 403/404 error pages.
+#1848622 by DamienMcKenna: Translation helper for 'bar'.
+#1967856 by duozersk: Support for the noimageindex and notranslate robots tag
+ options.
+#2140463 by zhuber: Small misspelling in a comment.
+#1963678 by DC_Marc, gnuget, Albert Volkman: Additional Twitter Card meta tags.
+#2170363 by juampy: Incorrect data handling in DrupalDefaultMetaTag.
+#1286270 by DamienMcKenna: Provide options for disabling meta tags on specific
+ entity types or entity bundles, see README.txt for details.
+#2071649 by eelkeblok, DamienMcKenna: Verify the entity still exists when
+ loading meta tag data in metatag_ctools_render_alter() and
+ metatag_views_post_render().
+#2126157 by hefox: metatag_entity_has_metatags() returns TRUE for disabled
+ entities, not FALSE.
+#2001178 by jantoine, DamienMcKenna: Verify the language exists before saving.
+#1864306 by hefox: Export the 'disabled' state via Features, thus allowing
+ disabled configurations to be exported too.
+#2172883 by Kristen Pol, DamienMcKenna: Only use Workbench Moderation functions
+ on nodes.
+#1975552 by pivica, DamienMcKenna: Fixed errors when changing {metatag} table's
+ primary keys.
+#1864306 by DamienMcKenna: Follow-on to only export the $config->disabled
+ setting if it exists.
+#2173271 by deetergp: Spelling and grammer fixes for README.txt.
+#2172433 by fabsor, DamienMcKenna: Ensure that update 7015 runs early enough to
+ avoid data corruption or errors during other updates.
+#2156261 by plopesc, DamienMcKenna: Allow meta tags for 403/404 error pages to
+ be configured, along with some reasonable defaults; removed previous option to
+ control caching on these pages, the meta tags are now always cached.
+#2173863 by DamienMcKenna: Don't load meta tags on admin pages, provide setting
+ to override this.
+#2174363 by DamienMcKenna: Don't attempt to create revision records in update
+ 7018 if one already exists.
Metatag 7.x-1.0-beta7, 2013-04-22
diff --git a/sites/all/modules/contrib/seo/metatag/README.txt b/sites/all/modules/contrib/seo/metatag/README.txt
index 8fbcd045..6bec0398 100644
--- a/sites/all/modules/contrib/seo/metatag/README.txt
+++ b/sites/all/modules/contrib/seo/metatag/README.txt
@@ -8,9 +8,10 @@ meta tags may help improve your site's & pages' ranking, thus may aid with
achieving a more prominent display of your content within search engine
results. Additionally, using meta tags can help control the summary content
that is used within social networks when visitors link to your site,
-particularly the Open Graph submodule for use with Facebook (see below).
+particularly the Open Graph submodule for use with Facebook, LinkedIn, etc (see
+below).
-This version of the module only works with Drupal 7.15 and newer.
+This version of the module only works with Drupal 7.28 and newer.
Features
@@ -18,7 +19,7 @@ Features
The primary features include:
* The current supported basic meta tags are ABSTRACT, DESCRIPTION, CANONICAL,
- COPYRIGHT, GENERATOR, IMAGE_SRC, KEYWORDS, PUBLISHER, REVISIT-AFTER, ROBOTS,
+ GENERATOR, IMAGE_SRC, KEYWORDS, PUBLISHER, REVISIT-AFTER, RIGHTS, ROBOTS,
SHORTLINK and the page's TITLE tag.
* Multi-lingual support using the Entity Translation module.
@@ -26,7 +27,7 @@ The primary features include:
* Translation support using the Internationalization (i18n) module.
* Full support for entity revisions and workflows based upon revision editing,
- e.g. Revisioning module.
+ e.g., Revisioning module.
* Per-path control over meta tags using the "Metatag: Context" submodule
(requires the Context module).
@@ -41,12 +42,21 @@ The primary features include:
* The fifteen Dublin Core Basic Element Set 1.1 meta tags may be added by
enabling the "Metatag: Dublin Core" submodule.
-* The Open Graph Protocol meta tags, as used by Facebook, may be added by
- enabling the "Metatag: Open Graph" submodule.
+* The Open Graph Protocol meta tags, as used by Facebook, LinkedIn and other
+ sites, may be added by enabling the "Metatag: Open Graph" submodule.
* The Twitter Cards meta tags may be added by enabling the "Metatag: Twitter
Cards" submodule.
+* Certain meta tags used by Google+ may be added by enabling the "Metatag:
+ Google+" submodule.
+
+* Facebook's fb:app_id and fb:admins meta tags may be added by enabling the
+ "Metatag: Facebook" submodule. These are useful for sites which are using
+ Facebook widgets or are building custom integration with Facebook's APIs,
+ but they are not needed by most sites and have no bearing on the Open Graph
+ meta tags.
+
* An API allowing for additional meta tags to be added, beyond what is provided
by this module - see metatag.api.php for full details.
@@ -56,6 +66,17 @@ The primary features include:
* Support for the Feeds module for importing data from external data sources or
file uploads.
+* Integrates with Devel_Generate, part of the Devel module, to automatically
+ generate meta tags for generated nodes, via the Metatag:Devel submodule.
+
+* Integrates with Workbench Moderation (both v1 and v2) allowing meta tags on
+ nodes to be managed through the workflow process.
+
+* The Transliteration and Imagecache Token modules (see below) are highly
+ recommended when using image meta tags, e.g. og:image.
+
+* Several advanced options may be controlled via the Advanced Settings page.
+
Configuration
------------------------------------------------------------------------------
@@ -68,7 +89,7 @@ Configuration
- The "Edit meta tags" permission to the roles that are allowed to change
meta tags on each individual page (node, term, etc).
- 2. The main admininistrative page controls the site-wide defaults, both global
+ 2. The main administrative page controls the site-wide defaults, both global
settings and defaults per entity (node, term, etc), in addition to those
assigned specifically for the front page:
admin/config/search/metatags
@@ -81,22 +102,32 @@ Configuration
4. As the meta tags are output using Tokens, it may be necessary to customize
the token display for the site's entities (content types, vocabularies,
- etc). To do this go to e.g. admin/structure/types/manage/article/display, in
- the "Custom Display Settings" section ensure that "Tokens" is checked (save
- the form if necessary), then to customize the tokens go to:
+ etc). To do this go to e.g., admin/structure/types/manage/article/display,
+ in the "Custom Display Settings" section ensure that "Tokens" is checked
+ (save the form if necessary), then to customize the tokens go to:
admin/structure/types/manage/article/display/token
Internationalization: i18n.module
------------------------------------------------------------------------------
All default configurations may be translated using the Internationalization
-(i18n) module. The custom strings that are assigned to e.g. the "Global: Front
+(i18n) module. The custom strings that are assigned to e.g., the "Global: Front
page" configuration will show up in the Translate Interface admin page
(admin/config/regional/translate/translate) and may be customized per language.
Fine Tuning
------------------------------------------------------------------------------
+All of these may be controlled from the advanced settings page:
+admin/config/search/metatags/settings
+
+* It is possible to "disable" the meta tags provided by Drupal core, i.e.
+ "generator", "canonical URL" and "shortlink", though it may not be completely
+ obvious. Metatag takes over the display of these tags, thus any changes made
+ to them in Metatag will supercede Drupal's normal output. To hide a tag, all
+ that is necessary is to clear the default value for that tag, e.g. on the
+ global settings for nodes, which will result in the tag not being output for
+ those pages.
* By default Metatag will load the global default values for all pages that do
not have meta tags assigned via the normal entity display or via Metatag
Context. This may be disabled by setting the variable 'metatag_load_all_pages'
@@ -114,6 +145,36 @@ Fine Tuning
required otherwise none of the meta tag fields will display at all. The
functionality may be disabled again by either removing the variable or
setting it to FALSE.
+* It's possible to disable Metatag integration for certain entity types or
+ bundles using variables. To disable an entity just assigning a variable
+ 'metatag_enable_{$entity_type}' or 'metatag_enable_{$entity_type}__{$bundle}'
+ the value FALSE, e.g.:
+ // Disable metatags for file_entity.
+ $conf['metatag_enable_file'] = FALSE;
+ // Disable metatags for carousel nodes.
+ $conf['metatag_enable_node__carousel'] = FALSE;
+ To enable the entity and/or bundle simply set the value to TRUE or remove the
+ settings.php line. Note that the Metatag cache will need to be cleared after
+ changing these settings, specifically the 'info' records, e.g., 'info:en'; a
+ quick version of doing this is to clear the site caches using either Drush,
+ Admin Menu (flush all caches), or the "Clear all caches" button on
+ admin/config/development/performance.
+* By default Metatag will not display meta tags on admin pages. To enable meta
+ tags on admin pages simply set the 'metatag_tag_admin_pages' variable to TRUE
+ through one of the following methods:
+ * Use Drush to set the value:
+ drush vset metatag_tag_admin_pages TRUE
+ * Hardcode the value in the site's settings.php file:
+ $conf['metatag_tag_admin_pages'] = TRUE;
+ To re-enable this option simply set the value to FALSE or delete the
+ settings.php line.
+* When loading an entity with multiple languages for a specific language the
+ meta tag values saved for that language will be used if they exist, otherwise
+ values assigned to the entity's default language will be used. This
+ may be disabled using the enabling the "Don't load entity's default language
+ values if no languages match" option on the Advanced Settings page, which will
+ cause default values to be used should there not be any values assigned for
+ the current requested language.
Developers
@@ -124,10 +185,19 @@ To enable Metatag support in custom entities, add 'metatag' => TRUE to either
the entity or bundle definition in hook_entity_info(); see metatag.api.php for
further details and example code.
+The meta tags for a given entity object (node, etc) can be obtained as follows:
+ $metatags = metatags_get_entity_metatags($entity_id, $entity_type, $langcode);
+The result will be a nested array of meta tag structures ready for either output
+via drupal_render(), or examining to identify the actual text values.
+
Troubleshooting / Known Issues
------------------------------------------------------------------------------
-* When using custom page template files, e.g. page--front.tpl.php, it is
+* Image fields do not output very easily in meta tags, e.g. for og:image,
+ without use of the Imagecache Token module (see below). This also provides a
+ way of using an image style to resize the original images first, rather than
+ requiring visitors download multi-megabyte original images.
+* When using custom page template files, e.g., page--front.tpl.php, it is
important to ensure that the following code is present in the template file:
or
@@ -137,17 +207,11 @@ Troubleshooting / Known Issues
taxonomy term pages to work correctly.
* Using Metatag with values assigned for the page title and the Page Title
module simultaneously can cause conflicts and unexpected results.
-* Using the Exclude Node Title module will cause the [node:title] token to be
- empty on node pages, so using [current-page:title] will work around the
- issue. Note: it isn't possible to "fix" this as it's a by-product of what
- Exclude Node Title does - it removes the node title from display.
* When customizing the meta tags for user pages, it is strongly recommended to
not use the [current-user] tokens, these pertain to the person *viewing* the
- page and not e.g. the person who authored a page.
-* If images being displayed in image tags need to be resized to fit a specific
- requirements, use the Imagecache Token module to customize the value.
-* Certain browser plugins, e.g. on Chrome, can cause the page title so be
- displayed with additional doublequotes, e.g. instead of:
+ page and not e.g., the person who authored a page.
+* Certain browser plugins, e.g., on Chrome, can cause the page title to be
+ displayed with additional double quotes, e.g., instead of:
The page title | My cool site
it will show:
"The page title | My cool site"
@@ -157,31 +221,45 @@ Troubleshooting / Known Issues
tags output by the Metatag:OpenGraph module. Unless it is actually needed for
the site, it may be worthwhile to disable the RDF module to avoid any
possible problems for the Open Graph integration.
+* If the Administration Language (admin_language) module is installed, it is
+ recommended to disable the "Force language neutral aliases" setting on the
+ Admin Language settings page, i.e. set the "admin_language_force_neutral"
+ variable to FALSE. Failing to do so can lead to data loss in Metatag.
Related modules
------------------------------------------------------------------------------
Some modules are available that extend Metatag with additional functionality:
+* Imagecache Token
+ https://www.drupal.org/project/imagecache_token
+ Provides additional tokens for image fields that can be used in e.g. the
+ og:image meta tag; ultimately makes it possible to actually use image meta
+ tags without writing custom code.
+
+* Transliteration
+ https://drupal.org/project/transliteration
+ Tidies up filenames for uploaded files, e.g. it can remove commas from
+ filenames that could otherwise break certain meta tags.
+
* Domain Meta Tags
- http://drupal.org/project/domain_meta
+ https://drupal.org/project/domain_meta
Integrates with the Domain Access module, so each site of a multi-domain
install can separately control their meta tags.
* Select or Other
- http://drupal.org/project/select_or_other
+ https://drupal.org/project/select_or_other
Enhances the user experience of the metatag_opengraph submodule by allowing
the creation of custom Open Graph types.
-* Imagecache Token
- http://drupal.org/project/imagecache_token
- Provide tokens to load fields using an image style preset, for when meta tags
- need to fix exact requirements.
-
* Node Form Panes
https://drupal.org/project/node_form_panes
Create custom node-edit forms and control the location of the Metatag fields.
+* Textimage
+ https://drupal.org/project/textimage
+ Supports using Textimage's custom tokens in meta tag fields.
+
Credits / Contact
------------------------------------------------------------------------------
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.admin.css b/sites/all/modules/contrib/seo/metatag/metatag.admin.css
index b7767263..3b463f6f 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.admin.css
+++ b/sites/all/modules/contrib/seo/metatag/metatag.admin.css
@@ -80,3 +80,9 @@ div.metatag-config-details ul li {
div.metatag-config-details > p {
margin: 0.25em 0;
}*/
+
+
+/* For the advanced settings page */
+.metatag-bundle-checkbox {
+ margin-left: 2em;
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.admin.inc b/sites/all/modules/contrib/seo/metatag/metatag.admin.inc
index 42a43a93..9138d038 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.admin.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.admin.inc
@@ -308,6 +308,10 @@ function metatag_config_edit_form($form, &$form_state, $config) {
metatag_metatags_form($form, $config->instance, $config->config, $options);
$form['metatags']['#type'] = 'container';
+ // Make the token browser available.
+ $form['metatags']['#prefix'] = $form['metatags'][LANGUAGE_NONE]['#description'];
+ unset($form['metatags'][LANGUAGE_NONE]['#description']);
+
$form['actions']['#type'] = 'actions';
$form['actions']['save'] = array(
'#type' => 'submit',
@@ -328,8 +332,8 @@ function metatag_config_edit_form_submit($form, &$form_state) {
form_state_values_clean($form_state);
$config = (object) $form_state['values'];
// @todo Consider renaming the config field from 'config' to 'metatags'
- $config->config = $config->metatags;
- unset($config->metatags);
+ $config->config = $config->metatags[LANGUAGE_NONE];
+ unset($config->metatags[LANGUAGE_NONE]);
metatag_config_save($config);
$label = metatag_config_instance_label($config->instance);
@@ -490,23 +494,23 @@ function metatag_bulk_revert_batch_operation($entity_type, $bundle, &$context) {
// Process 25 entities per iteration.
$query->range(0, 25);
$result = $query->execute();
- $ids = !empty($result[$entity_type]) ? array_keys($result[$entity_type]) : array();
- foreach ($ids as $id) {
- $metatags = metatag_metatags_load($entity_type, $id);
+ $entity_ids = !empty($result[$entity_type]) ? array_keys($result[$entity_type]) : array();
+ foreach ($entity_ids as $entity_id) {
+ $metatags = metatag_metatags_load($entity_type, $entity_id);
if (!empty($metatags)) {
db_delete('metatag')->condition('entity_type', $entity_type)
- ->condition('entity_id', $id)
+ ->condition('entity_id', $entity_id)
->execute();
- metatag_metatags_cache_clear($entity_type, $id);
+ metatag_metatags_cache_clear($entity_type, $entity_id);
$context['results'][] = t('Reverted metatags for @bundle with id @id.', array(
'@bundle' => $entity_type . ': ' . $bundle,
- '@id' => $id,
+ '@id' => $entity_id,
));
}
}
- $context['sandbox']['count'] += count($ids);
- $context['sandbox']['current'] = max($ids);
+ $context['sandbox']['count'] += count($entity_ids);
+ $context['sandbox']['current'] = max($entity_ids);
if ($context['sandbox']['count'] != $context['sandbox']['total']) {
$context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
@@ -532,3 +536,115 @@ function metatag_bulk_revert_batch_finished($success, $results, $operations) {
array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
}
}
+
+/**
+ * Misc settings page.
+ */
+function metatag_admin_settings_form() {
+ $form = array();
+
+ $form['#attached'] = array(
+ 'js' => array(
+ drupal_get_path('module', 'metatag') . '/metatag.admin.js',
+ ),
+ 'css' => array(
+ drupal_get_path('module', 'metatag') . '/metatag.admin.css',
+ ),
+ );
+
+ $form['advanced'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Advanced settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ $form['advanced']['metatag_load_all_pages'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Output meta tags even if only global settings apply'),
+ '#description' => t('By default Metatag will load the global default values for all pages that do not have meta tags assigned via the normal entity display or via Metatag Context. This may be disabled so that meta tags will only be output on pages that specifically have meta tags configured for them.'),
+ '#default_value' => variable_get('metatag_load_all_pages', TRUE),
+ );
+
+ $form['advanced']['metatag_tag_admin_pages'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Output meta tags on admin pages'),
+ '#description' => t('By default meta tags will not be output on admin pages, but it may be beneficial for some sites to do so.'),
+ '#default_value' => variable_get('metatag_tag_admin_pages', FALSE),
+ );
+
+ $form['advanced']['metatag_extended_permissions'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Advanced permissions'),
+ '#description' => t('Optionally add a permission for each individual meta tag. This provides tremendous flexibility for the editorial process, at the expense of making the permissions configuration more tedious.'),
+ '#default_value' => variable_get('metatag_extended_permissions', FALSE),
+ );
+
+ $form['advanced']['metatag_entity_no_lang_default'] = array(
+ '#type' => 'checkbox',
+ '#title' => t("Don't load entity's default language values if no languages match"),
+ '#description' => t("On a site with multiple languages it is possible for an entity to not have meta tag values assigned for the language of the current page. By default the meta tags for an entity's default language value will be used in this scenario, with the canonical URL pointing to the URL. This option may be disabled, i.e. to only load meta tags for languages that specifically have them assigned, otherwise using defaults."),
+ '#default_value' => variable_get('metatag_entity_no_lang_default', FALSE),
+ );
+
+ $form['entities'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Master controls for all entities'),
+ '#description' => t('By enabling and disabling items here, it is possible to control which entities (e.g. nodes, taxonomy terms) and bundles (e.g. content types, vocabularies) will have meta tags available to them.
Note: the entities first have to have meta tags enabled via hook_entity_info; see the API documentation for full details.'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+
+ foreach (entity_get_info() as $entity_name => $entity_info) {
+ // Only show entities that have been enabled via the hooks.
+ if (!empty($entity_info['metatags'])) {
+ $form['entities']['metatag_enable_' . $entity_name] = array(
+ '#type' => 'checkbox',
+ '#title' => t($entity_info['label']),
+ '#default_value' => variable_get('metatag_enable_' . $entity_name, TRUE),
+ '#description' => t('Enable meta tags for all pages this entity type.'),
+ );
+
+ if (!empty($entity_info['bundles'])) {
+ $desc_added = FALSE;
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ // Some entities, e.g. User, (core) File, have a bundle with the same
+ // name as the entity, so
+ if ($bundle_name != $entity_name) {
+ // Add an extra line to the description introducing the entity
+ // bundles.
+ if (!$desc_added) {
+ $form['entities']['metatag_enable_' . $entity_name]['#disabled'] = TRUE;
+ $form['entities']['metatag_enable_' . $entity_name]['#default_value'] = TRUE;
+ $form['entities']['metatag_enable_' . $entity_name]['#description'] = t('Each bundle for this entity must be controlled individually.');
+ $desc_added = TRUE;
+ }
+ $form['entities']['metatag_enable_' . $entity_name . '__' . $bundle_name] = array(
+ '#type' => 'checkbox',
+ '#title' => t($bundle_info['label']),
+ '#default_value' => variable_get('metatag_enable_' . $entity_name . '__' . $bundle_name, isset($bundle_info['metatags']) ? $bundle_info['metatags'] : TRUE),
+ '#attributes' => array(
+ // Add some theming that'll indent this bundle.
+ 'class' => array('metatag-bundle-checkbox'),
+ ),
+ );
+ }
+ }
+ }
+ }
+ }
+
+ // Extra submission logic.
+ $form['#submit'][] = 'metatag_admin_settings_form_submit';
+
+ return system_settings_form($form);
+}
+
+/**
+ * Form API submission callback for metatag_admin_settings_form().
+ */
+function metatag_admin_settings_form_submit() {
+ cache_clear_all('entity_info:', 'cache', TRUE);
+ cache_clear_all('*', 'cache_metatag', TRUE);
+ drupal_set_message(t('The Metatag cache has been cleared, so all meta tags can be reloaded.'));
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.admin.js b/sites/all/modules/contrib/seo/metatag/metatag.admin.js
index c213f721..64e6778f 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.admin.js
+++ b/sites/all/modules/contrib/seo/metatag/metatag.admin.js
@@ -1,13 +1,19 @@
+/**
+ * @file
+ * Custom admin-side JS for the Metatag module.
+ */
+
(function ($) {
+ 'use strict';
Drupal.behaviors.metatagUIConfigListing = {
attach: function (context) {
- // Hide elements to be visible if JavaScript is enabled.
+ // Hidden elements to be visible if JavaScript is enabled.
$('.js-show').show();
// Make the leaf arrow clickable.
$('.metatag-config-label').hover(function(){
- $(this).css({'cursor':'pointer'});
+ $(this).css({'cursor': 'pointer'});
})
.click(function(){
$(this).find('a.toggle-details', context).trigger('click');
@@ -37,6 +43,6 @@ Drupal.behaviors.metatagUIConfigListing = {
event.stopPropagation();
});
}
-}
+};
})(jQuery);
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.api.php b/sites/all/modules/contrib/seo/metatag/metatag.api.php
index 9b1603c3..bdcb4dd5 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.api.php
+++ b/sites/all/modules/contrib/seo/metatag/metatag.api.php
@@ -114,6 +114,21 @@ function hook_metatag_config_default() {
return $configs;
}
+/**
+ * Internal hook for adding further configuration values in bundled submodules.
+ *
+ * The defaults provided by the main Metatag module need to be extended by the
+ * bundled submodules before they are presented to other modules for altering
+ * via hook_metatag_config_default_alter(), in case differences in module
+ * weights and loading priorities cause the submodules' settings to run after
+ * those of any custom modules.
+ *
+ * @see hook_metatag_config_default()
+ * @see hook_metatag_config_default_alter()
+ */
+function hook_metatag_bundled_config_alter(&$config) {
+}
+
/**
*
*/
@@ -170,7 +185,74 @@ function hook_metatag_config_update($config) {
}
/**
- *
+ * Definition of the meta tags and groups.
+ *
+ * @return array
+ * A nested array of 'tags' and 'groups', each keyed off the machine name for
+ * their respective structure type, with the following values:
+ * Tags:
+ * 'label' - The name for this meta tag.
+ * 'description' - An explanation of what this meta tag is used for and what
+ * values are permissible.
+ * 'class' - The class name that controls this meta tag.
+ * 'weight' - Used to sort the meta tags during output.
+ * 'group' - The machine name of a group this meta tag will be contained
+ * within.
+ * 'context' - Optionally control the type of configuration the meta tag
+ * will be available from. Possible values are:
+ * [empty] - All meta tags apply to all possible objects, by default.
+ * 'global' - This will make it only show in the global meta tag
+ * configuration form.
+ * [entity type] - Makes the meta tag only show for specific entity types.
+ * 'header' - Optionally output the meta tag as an HTTP header value.
+ * 'element' - Optional attributes for rendering the meta tag. Should
+ * contain the following:
+ * '#theme' - The theming function used to render the meta tag.
+ * 'replaces' - An optional array of meta tags that this meta tag replaces.
+ * Used to indicate that these deprecated meta tags will be replaced by
+ * this newer one, their values will be used, upon the next object save
+ * the deprecated tag will be entirely replaced by the new meta tag. While
+ * one meta tag can replace several others, only one of the possible
+ * values will be used, the others will be silently purged upon the next
+ * configuration/object save.
+ * 'form' - Optional items to be passed directly to the form; uses standard
+ * Form API values.
+ * 'devel_generate' - Optional values to be passed to the Devel Generate
+ * submodule. Should be an array containing one of the following values:
+ * 'type' - One of the following:
+ * 'canonical' - The token for the absolute URL for the current page.
+ * 'email' - An email address randomly generated at the site's hostname.
+ * 'float' - A random floating point number between 0.0 and 999.999.
+ * 'image' - A randomly generated image.
+ * 'integer' - A random integer between 0 and 999.
+ * 'phone' - A phone number in the format 999-999-9999.
+ * 'select' - A value randomly selected from the available form options.
+ * 'text' - Random text string.
+ * 'twitter' - A Twitter username.
+ * 'url' - A randomly generated URL on this site.
+ * 'maxlength' - The maximum length / number of iterations of this value,
+ * defaults to 10.
+ * 'dependencies' - Optional nested array of values to indicate other meta
+ * tags that must be present in order for this meta tag to be visible. See
+ * The Open Graph and Twitter Cards submodules for example usage. Each
+ * dependency must contain the following elements:
+ * 'dependency' - The name of the meta tag that is required.
+ * 'attribute' - The name of the other meta tag that is to be checked,
+ * most meta tags use "value" as the attribute name.
+ * 'condition' - The state condition to be checked against, e.g. "filled"
+ * to check text values, "checked" for a checkbox, "value" to compare
+ * the raw selection; see https://api.drupal.org/drupal_process_states
+ * for more details.
+ * 'value' - The field value to check the 'condition' against. see
+ * https://api.drupal.org/drupal_process_states for further details.
+ * Groups:
+ * 'label' - The name for this group.
+ * 'description' - A detailed explanation of these meta tags.
+ * 'form' - Additional items to be passed directly to the form.
+ * Note: 'label', 'description', and any text strings passed in 'form', should
+ * be translated.
+ *
+ * @see metatag_metatag_info().
*/
function hook_metatag_info() {
return array();
@@ -199,8 +281,10 @@ function hook_metatag_load_entity_from_path_alter(&$path, $result) {
* @param string $instance
* An identifier for the current page's page type, typically a combination
* of the entity name and bundle name, e.g. "node:story".
+ * @param array $options
+ * All of the options used to generate the meta tags.
*/
-function hook_metatag_metatags_view_alter(&$output, $instance) {
+function hook_metatag_metatags_view_alter(&$output, $instance, $options) {
if (isset($output['description']['#attached']['drupal_add_html_head'][0][0]['#value'])) {
$output['description']['#attached']['drupal_add_html_head'][0][0]['#value'] = 'O rly?';
}
@@ -270,10 +354,12 @@ function hook_metatag_token_types_alter(&$options) {
* user object being the value. Some token types, like 'site', do not require
* any explicit information from $data and can be replaced even if it is
* empty.
+ * @param string $tag_name
+ * The name of the meta tag being altered.
*
* @see DrupalTextMetaTag::getValue()
*/
-function hook_metatag_pattern_alter(&$pattern, &$types) {
+function hook_metatag_pattern_alter(&$pattern, &$types, $tag_name) {
if (strpos($pattern, 'token_type1') !== FALSE) {
$types['token_type1'] = "data to be used in hook_tokens for replacement";
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.drush.inc b/sites/all/modules/contrib/seo/metatag/metatag.drush.inc
new file mode 100644
index 00000000..950e5607
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag.drush.inc
@@ -0,0 +1,21 @@
+instance = $config->instance;
+ if (isset($config->disabled)) {
+ $export->disabled = $config->disabled;
+ }
$export->config = $config->config;
$export = features_var_export($export, ' ');
$key = features_var_export($name);
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.feeds.inc b/sites/all/modules/contrib/seo/metatag/metatag.feeds.inc
index 55578bfa..00a91c59 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.feeds.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.feeds.inc
@@ -32,22 +32,32 @@ function metatag_feeds_set_target($source, $entity, $target, $value) {
// Strip the prefix that was added above.
$name = str_replace('meta_', '', $target);
- // Load full metatag object if exists and has not been already loaded.
+ // Check for any existing data that may not have been loaded already.
if (!isset($entity->metatags) && !empty($entity->feeds_item->entity_id) && is_numeric($entity->feeds_item->entity_id)) {
+ $entity->metatags = array();
$entity_type = $entity->feeds_item->entity_type;
$entity_id = $entity->feeds_item->entity_id;
- $entities = entity_load($entity_type, array($entity_id));
- if (!empty($entities)) {
- $stored_entity = reset($entities);
- $metatags = metatag_metatags_load($entity_type, $entity_id);
- if (!empty($stored_entity) && !empty($metatags)) {
- $stored_entity_language = metatag_entity_get_language($entity_type, $stored_entity);
- $entity->metatags = isset($metatags[$stored_entity_language]) ? $metatags[$stored_entity_language] : array();
+
+ // Load the existing entity.
+ $stored_entities = entity_load($entity_type, array($entity_id));
+ if (!empty($stored_entities)) {
+ $stored_entity = reset($stored_entities);
+ if (!empty($stored_entity)) {
+ // This function returns all values for all revisions.
+ $metatags = metatag_metatags_load($entity_type, $entity_id);
+ if (!empty($metatags)) {
+ // Only load meta tags for the current revision.
+ list(, $revision_id) = entity_extract_ids($entity_type, $stored_entity);
+ if (!empty($metatags[$revision_id])) {
+ // Just copy all meta tags for all languages.
+ $entity->metatags = $metatags[$revision_id];
+ }
+ }
}
}
}
// Assign the value.
- $entity->metatags[$name]['value'] = $value;
+ $langcode = metatag_entity_get_language($entity_type, $entity);
+ $entity->metatags[$langcode][$name]['value'] = $value;
}
-
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.inc b/sites/all/modules/contrib/seo/metatag/metatag.inc
index 529c1ba9..0a575178 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.inc
@@ -18,6 +18,8 @@ interface DrupalMetaTagInterface {
function getValue();
+ function getWeight();
+
function getElement();
function tidyValue($value);
@@ -27,6 +29,7 @@ class DrupalDefaultMetaTag implements DrupalMetaTagInterface {
protected $info;
protected $data = array('value' => '');
+ protected $weight = 0;
function __construct(array $info, array $data = NULL) {
$this->info = $info;
@@ -35,38 +38,87 @@ class DrupalDefaultMetaTag implements DrupalMetaTagInterface {
}
}
+ /**
+ * Calculate the weight or this meta tag.
+ *
+ * @return integer
+ */
+ public function getWeight() {
+ static $counter = 0;
+
+ // If no weight value is found, stack this meta tag at the end.
+ $weight = 100;
+ if (!empty($this->info['weight'])) {
+ $weight = $this->info['weight'];
+ }
+
+ return $weight + ($counter++ * 0.1);
+ }
+
public function getForm(array $options = array()) {
return array();
}
public function getValue(array $options = array()) {
- return tidyValue($this->data['value']);
+ return $this->tidyValue($this->data['value']);
}
public function getElement(array $options = array()) {
- $element = isset($this->info['element']) ? $this->info['element'] : array();
-
$value = $this->getValue($options);
if (strlen($value) === 0) {
return array();
}
- $element += array(
- '#theme' => 'metatag',
- '#tag' => 'meta',
- '#id' => 'metatag_' . $this->info['name'],
- '#name' => $this->info['name'],
- '#value' => $value,
- );
+ // The stack of elements that will be output.
+ $elements = array();
- // Add header information if desired.
- if (!empty($this->info['header'])) {
- $element['#attached']['drupal_add_http_header'][] = array($this->info['header'], $value);
+ // Dynamically add each option to this setting.
+ $base_element = isset($this->info['element']) ? $this->info['element'] : array();
+
+ // Single item.
+ if (empty($this->info['multiple'])) {
+ $values = array($value);
}
- return array(
- '#attached' => array('drupal_add_html_head' => array(array($element, $element['#id']))),
- );
+ // Multiple items.
+ else {
+ $values = array_filter(explode(',', $value));
+ }
+
+ // Loop over each item.
+ if (!empty($values)) {
+ foreach ($values as $ctr => $value) {
+ $value = trim($value);
+
+ // Some meta tags must be output as secure URLs.
+ if (!empty($this->info['secure'])) {
+ $value = str_replace('http://', 'https://', $value);
+ }
+
+ // Combine the base configuration for this meta tag with the value.
+ $element = $base_element + array(
+ '#theme' => 'metatag',
+ '#tag' => 'meta',
+ '#id' => 'metatag_' . $this->info['name'] . '_' . $ctr,
+ '#name' => $this->info['name'],
+ '#value' => $value,
+ '#weight' => $this->getWeight(),
+ );
+
+ // Add header information if desired.
+ if (!empty($this->info['header'])) {
+ $element['#attached']['drupal_add_http_header'][] = array($this->info['header'], $value);
+ }
+
+ $elements[] = array($element, $element['#id']);
+ }
+ }
+
+ if (!empty($elements)) {
+ return array(
+ '#attached' => array('drupal_add_html_head' => $elements),
+ );
+ }
}
/**
@@ -79,6 +131,11 @@ class DrupalDefaultMetaTag implements DrupalMetaTagInterface {
* The meta tag value after it has been tidied up.
*/
public function tidyValue($value) {
+ // Specifically replace encoded spaces, because some WYSIWYG editors are
+ // silly. Do this before decoding the other HTML entities so that the output
+ // doesn't end up with a bunch of a-circumflex characters.
+ $value = str_replace(' ', ' ', $value);
+
// Convert any HTML entities into regular characters.
$value = decode_entities($value);
@@ -117,6 +174,21 @@ class DrupalTextMetaTag extends DrupalDefaultMetaTag {
'#maxlength' => 1024,
);
+ if (!empty($this->info['multiple'])) {
+ $form['value']['#description'] .= ' ' . t('Multiple values may be used, separated by a comma. Note: Tokens that return multiple values will be handled automatically.');
+ }
+
+ // Support for dependencies, using Form API's #states system.
+ // @see metatag.api.php.
+ // @see https://api.drupal.org/drupal_process_states
+ if (!empty($this->info['dependencies'])) {
+ foreach ($this->info['dependencies'] as $specs) {
+ $form['value']['#states']['visible'][':input[name*="[' . $specs['dependency'] . '][' . $specs['attribute'] . ']"]'] = array(
+ $specs['condition'] => $specs['value'],
+ );
+ }
+ }
+
return $form;
}
@@ -135,7 +207,7 @@ class DrupalTextMetaTag extends DrupalDefaultMetaTag {
if (empty($options['raw'])) {
// Give other modules the opportunity to use hook_metatag_pattern_alter()
// to modify defined token patterns and values before replacement.
- drupal_alter('metatag_pattern', $value, $options['token data']);
+ drupal_alter('metatag_pattern', $value, $options['token data'], $this->info['name']);
$value = token_replace($value, $options['token data'], $options);
}
return $this->tidyValue($value);
@@ -161,12 +233,13 @@ class DrupalLinkMetaTag extends DrupalTextMetaTag {
'#id' => 'metatag_' . $this->info['name'],
'#name' => $this->info['name'],
'#value' => $value,
+ '#weight' => $this->getWeight(),
);
if (!isset($this->info['header']) || !empty($this->info['header'])) {
// Also send the generator in the HTTP header.
// @todo This does not support 'rev' or alternate link headers.
- $element['#attached']['drupal_add_http_header'][] = array('Link', '<' . check_plain($value) . '>;' . drupal_http_header_attributes(array('rel' => $element['#name'])), TRUE);
+ $element['#attached']['drupal_add_http_header'][] = array('Link', '<' . $value . '>;' . drupal_http_header_attributes(array('rel' => $element['#name'])), TRUE);
}
return array(
@@ -185,7 +258,7 @@ class DrupalTitleMetaTag extends DrupalTextMetaTag {
public function getElement(array $options = array()) {
$element = array();
- $value = check_plain($this->getValue($options));
+ $value = $this->getValue($options);
$element['#attached']['metatag_set_preprocess_variable'][] = array('html', 'head_title', $value);
$element['#attached']['metatag_set_preprocess_variable'][] = array('html', 'head_array', array('title' => $value));
return $element;
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.info b/sites/all/modules/contrib/seo/metatag/metatag.info
index 06be5e91..416cdf87 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.info
+++ b/sites/all/modules/contrib/seo/metatag/metatag.info
@@ -3,8 +3,9 @@ description = "Adds support and an API to implement meta tags."
package = SEO
core = 7.x
-; This requires Drupal 7.15 or newer.
-dependencies[] = system (>=7.15)
+; This requires Drupal 7.28 or newer as it fixes the [node:summary] token that
+; was previously broken.
+dependencies[] = system (>=7.28)
; CTools and Token are also required.
dependencies[] = ctools
@@ -16,9 +17,9 @@ files[] = metatag.inc
files[] = metatag.migrate.inc
files[] = metatag.test
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.install b/sites/all/modules/contrib/seo/metatag/metatag.install
index ae3f390f..73cfaa94 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.install
+++ b/sites/all/modules/contrib/seo/metatag/metatag.install
@@ -12,7 +12,48 @@ function metatag_requirements($phase) {
// Ensure translations don't break during installation.
$t = get_t();
- if ($phase == 'runtime') {
+ if ($phase == 'install') {
+ // Handle scenarios where the site had the legacy "metatags" module
+ // installed but then had Metatag installed on top of it.
+ if (function_exists('db_table_exists') && function_exists('db_field_exists') && function_exists('db_query') && Database::isActiveConnection()) {
+ // Check if the primary table already exists.
+ if (db_table_exists('metatag')) {
+ // Check to see if all of the fields exist in the table. If one of the
+ // fields does not exist, proceed with the fix.
+ $fields = array(
+ 'entity_type',
+ 'entity_id',
+ 'revision_id',
+ 'language',
+ 'data',
+ );
+ foreach ($fields as $field) {
+ // This field doesn't exist, so determine what to do.
+ if (!db_field_exists('metatag', $field)) {
+ // The table contains data, so rename it.
+ if (db_query("SELECT COUNT(*) FROM {metatag}")->fetchField() > 0) {
+ db_query("RENAME TABLE {metatag} TO {metatag}_legacy");
+ $message = 'An out-of-date version of the {metatag} table was discovered. As the table contained data it was renamed with a suffix of "_legacy". This will not prevent installation from continuing, but will need to be dealt with later. See https://www.drupal.org/node/1391554 for further details.';
+ }
+ // The table is empty, so delete it.
+ else {
+ db_query("DROP TABLE {metatag}");
+ $message = 'An out-of-date version of the {metatag} table was discovered. As the table was empty it was simply removed so that it could be recreated in the correct format. Installation may now proceed. See https://www.drupal.org/node/1391554 for further details.';
+ }
+ $requirements['metatag'] = array(
+ 'severity' => REQUIREMENT_WARNING,
+ 'title' => 'Metatag',
+ 'value' => $t('Legacy data discovered.'),
+ 'description' => $t($message),
+ );
+ drupal_set_message($t($message), 'warning');
+ break;
+ }
+ }
+ }
+ }
+ }
+ elseif ($phase == 'runtime') {
// Work out the release of D7 that is currently running.
list($major, $minor) = explode('.', VERSION);
// Strip off any suffixes on the version string, e.g. "17-dev".
@@ -20,24 +61,14 @@ function metatag_requirements($phase) {
list($minor, $suffix) = explode('-', $minor);
}
- // Releases of Drupal older than 7.15 did not have entity_language(), which
- // is now required.
- if ($minor < 15) {
+ // Releases of Drupal older than 7.28 did not have entity_language(), which
+ // is now required, and had a broken [node:summary] token.
+ if ($minor < 28) {
$requirements['metatag'] = array(
'severity' => REQUIREMENT_WARNING,
'title' => 'Metatag',
- 'value' => $t('Upgrade Drupal core to v7.15 or newer'),
- 'description' => $t("This older version of Drupal core is missing functionality necessary for the module's multilingual support, it must be upgraded to at least version 7.15."),
- );
- }
- // Releases of Drupal older than 7.17 did not trigger hook_entity_view on
- // term pages, so recommend updating.
- elseif ($minor < 17) {
- $requirements['metatag'] = array(
- 'severity' => REQUIREMENT_WARNING,
- 'title' => 'Metatag',
- 'value' => $t('Upgrade Drupal core to v7.17 or newer'),
- 'description' => $t('Your older version of Drupal core is missing functionality necessary for taxonomy term pages to work correctly, it is strongly recommended to upgrade to the latest release.'),
+ 'value' => $t('Upgrade Drupal core to v7.28 or newer'),
+ 'description' => $t("This older version of Drupal core is missing functionality necessary for the module's multilingual support and contains a broken [node:summary] token, it must be upgraded to version 7.28 or newer."),
);
}
// Everything's OK.
@@ -46,7 +77,7 @@ function metatag_requirements($phase) {
'severity' => REQUIREMENT_OK,
'title' => 'Metatag',
'value' => $t('Drupal core is compatible'),
- 'description' => $t('Older versions of Drupal core were missing functionality necessary for taxonomy term pages to work correctly, but this version will work correctly.'),
+ 'description' => $t('Older versions of Drupal core contained bugs that made them incompatible with Metatag, but this version will work correctly.'),
);
}
@@ -60,16 +91,6 @@ function metatag_requirements($phase) {
);
}
- // Add a note if Page Title is also installed.
- if (module_exists('exclude_node_title')) {
- $requirements['metatag_exclude_node_title'] = array(
- 'severity' => REQUIREMENT_INFO,
- 'title' => 'Metatag',
- 'value' => $t('Possible conflicts with Exclude Node Title module'),
- 'description' => $t('The Metatag module\'s default settings for content types (nodes) uses [node:title] for the page title. Unfortunately, Exclude Node Title hides this so the page title ends up blank. It is recommended to change the "title" field\'s default value to "[current-page:title]" instead of "[node:title]" for any content types affected by Exclude Node Title.', array('!config' => 'admin/config/search/metatags')),
- );
- }
-
// Add a note if the deprecated metatag.entity_translation.inc file still
// exists.
$filename = 'metatag.entity_translation.inc';
@@ -94,6 +115,38 @@ function metatag_requirements($phase) {
);
}
}
+
+ // It's recommended to install the Transliteration module to clean up file
+ // paths for use with image meta tags.
+ if (!module_exists('transliteration')) {
+ $requirements['metatag_transliteration'] = array(
+ 'severity' => REQUIREMENT_INFO,
+ 'title' => 'Metatag',
+ 'value' => $t('The Transliteration module is recommended.'),
+ 'description' => $t("It is recommended to install the Transliteration module to clean up filenames of uploaded files that may be used with image meta tags.", array('@url' => 'https://drupal.org/project/transliteration')),
+ );
+ }
+
+ // It's recommended to install the Imagecache Token module to make image
+ // tokens easier to do.
+ if (!module_exists('imagecache_token')) {
+ $requirements['metatag_imagecache_token'] = array(
+ 'severity' => REQUIREMENT_INFO,
+ 'title' => 'Metatag',
+ 'value' => $t('The Imagecache Token module is recommended.'),
+ 'description' => $t("It is recommended to install the Imagecache Token module to make it easier to control image meta tags, e.g. og:image.", array('@url' => 'https://drupal.org/project/imagecache_token')),
+ );
+ }
+
+ // The Admin Language module can cause problems.
+ if (!module_exists('admin_language') && variable_get('admin_language_force_neutral', 0)) {
+ $requirements['metatag_admin_language'] = array(
+ 'severity' => REQUIREMENT_WARNING,
+ 'title' => 'Metatag',
+ 'value' => $t('Conflict with Admin Language module.'),
+ 'description' => $t("Using the \"@option\" with Metatag can lead to data loss, so it is recommended to disable that option.", array('@option' => t('Force language neutral aliases'), '@url' => url('admin/config/regional/language/admin_language'))),
+ );
+ }
}
return $requirements;
@@ -169,7 +222,7 @@ function metatag_schema() {
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
- 'not null' => FALSE,
+ 'not null' => TRUE,
'default' => 0,
'description' => 'The revision_id for the entity object this data is attached to.',
),
@@ -187,6 +240,9 @@ function metatag_schema() {
'serialize' => TRUE,
),
),
+ 'indexes' => array(
+ 'type_revision' => array('entity_type','revision_id'),
+ ),
'primary key' => array('entity_type', 'entity_id', 'revision_id', 'language'),
);
@@ -200,7 +256,7 @@ function metatag_schema() {
* Implements hook_install().
*/
function metatag_install() {
- drupal_set_message(t("Thank you for installing the Metatag module. It is recommended to read the module's README.txt file as there are some known issues that may afffect this site.", array('!url' => url(drupal_get_path('module', 'metatag') . '/README.txt'))));
+ drupal_set_message(t("Thank you for installing the Metatag module. It is recommended to read the module's README.txt file as there are some known issues that may affect this site.", array('!url' => url(drupal_get_path('module', 'metatag') . '/README.txt'))));
}
/**
@@ -209,6 +265,21 @@ function metatag_install() {
function metatag_uninstall() {
// This variable is created via hook_enable.
variable_del('metatag_schema_installed');
+ // Used to control whether 403/404 pages are cached.
+ variable_del('metatag_cache_error_pages');
+ // Used to make meta tags display on admin pages.
+ variable_del('metatag_tag_admin_pages');
+
+ // Temp variables, just in case they weren't removed already.
+ variable_del('metatag_skip_update_7017');
+
+ // Used to note that the schema for the main {metatag} table were sufficiently
+ // updated.
+ variable_del('metatag_has_revision_id');
+
+ // Used to force an entity's default language values to be used if nothing
+ // else matched.
+ variable_del('metatag_entity_no_lang_default');
}
/**
@@ -248,9 +319,13 @@ function metatag_update_7001() {
);
$keys = array('primary key' => array($field_name));
- // Before making any changes, drop the existing primary key. Unforunately
- // there is no API way to check if a primary key exists, so if it doesn't
- // exist the db_drop_primary_key() call will fail.
+ // Before making any changes, drop the existing primary key.
+ // Let's add a temporary unique key for cid so MySQL will let it go.
+ // Hint taken from https://drupal.org/node/2064305#comment-7753197.
+ db_add_unique_key($table_name, 'temp_key', array($field_name, 'instance'));
+
+ // Unforunately there is no API way to check if a primary key exists, so if
+ // it doesn't exist the db_drop_primary_key() call will fail.
try {
db_drop_primary_key($table_name);
}
@@ -260,6 +335,9 @@ function metatag_update_7001() {
// Rejig the field, and turn on the primary key again.
db_change_field($table_name, $field_name, $field_name, $field_spec, $keys);
+
+ // Finally, remove the temporary unique key because it's no longer useful.
+ db_drop_unique_key($table_name, 'temp_key');
}
/**
@@ -351,8 +429,8 @@ function metatag_update_7006() {
}
/**
- * Remove {metatag} records for objects that have been deleted; older versions
- * of Metatag may have failed to purge these.
+ * Remove {metatag} records for nodes, users and taxonomy terms that have been
+ * deleted; older versions of Metatag may have failed to purge these.
*/
function metatag_update_7007() {
$nodes = db_query("SELECT m.entity_id
@@ -440,6 +518,9 @@ function metatag_update_7008() {
*/
function metatag_update_7009() {
if (module_exists('taxonomy')) {
+ // Fix the {metatag} table first.
+ metatag_update_7015();
+
// Remove duplicates.
_metatag_remove_dupes('taxonomy_term');
}
@@ -456,6 +537,9 @@ function metatag_update_7009() {
* Fix {metatag} records for users.
*/
function metatag_update_7010() {
+ // Fix the {metatag} table first.
+ metatag_update_7015();
+
// Remove duplicates.
_metatag_remove_dupes('user');
@@ -470,6 +554,9 @@ function metatag_update_7010() {
* Fix {metatag} records for nodes.
*/
function metatag_update_7011(&$sandbox) {
+ // Fix the {metatag} table first.
+ metatag_update_7015();
+
// Only proceed if Entity_Translation is not enabled as it allows each node
// record to have multiple languages available.
if (module_exists('entity_translation')) {
@@ -481,7 +568,12 @@ function metatag_update_7011(&$sandbox) {
// When a group is processed, the batch update engine determines whether it
// should continue processing in the same request or provide progress
// feedback to the user and wait for the next request.
- $limit = 20;
+ $limit = 10;
+ // When ran through Drush it's Ok to process a larger number of objects at a
+ // time.
+ if (drupal_is_cli()) {
+ $limit = 100;
+ }
// Use the sandbox at your convenience to store the information needed
// to track progression between successive calls to the function.
@@ -528,15 +620,8 @@ function metatag_update_7011(&$sandbox) {
$limit = 1;
}
- // The for loop will run as normal when ran via update.php, but when ran via
- // Drush it'll just run 'til it's finished.
- $increment = 1;
- if (drupal_is_cli()) {
- $increment = 0;
- }
-
// Set default values.
- for ($ctr = 0; $ctr < $limit; $ctr += $increment) {
+ for ($ctr = 0; $ctr < $limit; $ctr++) {
$sandbox['current_record']++;
if (empty($sandbox['records'][$sandbox['current_record']])) {
break;
@@ -581,6 +666,9 @@ function metatag_update_7011(&$sandbox) {
* Remove duplicate {metatag} records for non-core entities.
*/
function metatag_update_7012() {
+ // Fix the {metatag} table first.
+ metatag_update_7015();
+
if (module_exists('entity_translation')) {
drupal_set_message(t("Entity Translation is enabled, duplicate meta tags will not be removed for custom entities, to avoid accidental dataloss."));
return;
@@ -611,6 +699,9 @@ function metatag_update_7012() {
* take a while, depending on how much data needs to be converted.
*/
function metatag_update_7013(&$sandbox) {
+ // Fix the {metatag} table first.
+ metatag_update_7015();
+
if (module_exists('entity_translation')) {
drupal_set_message(t("Entity Translation is enabled, meta tags will not be updated for custom entities, to avoid accidental dataloss."));
return;
@@ -666,16 +757,14 @@ function metatag_update_7013(&$sandbox) {
// should continue processing in the same request or provide progress
// feedback to the user and wait for the next request.
$limit = 10;
-
- // The for loop will run as normal when ran via update.php, but when ran via
- // Drush it'll just run 'til it's finished.
- $increment = 1;
+ // When ran through Drush it's Ok to process a larger number of objects at a
+ // time.
if (drupal_is_cli()) {
- $increment = 0;
+ $limit = 100;
}
// Set default values.
- for ($ctr = 0; $ctr < $limit; $ctr += $increment) {
+ for ($ctr = 0; $ctr < $limit; $ctr++) {
$sandbox['current_record']++;
if (empty($sandbox['records'][$sandbox['current_record']])) {
break;
@@ -904,21 +993,559 @@ function metatag_update_7014() {
* keys accordingly.
*/
function metatag_update_7015() {
- // Add the new field.
- db_add_field('metatag', 'revision_id', array(
+ if (!db_field_exists('metatag', 'revision_id')) {
+ // Leave a note for metatag_metatags_load_multiple() that the revision_id
+ // field has been added.
+ variable_set('metatag_has_revision_id', TRUE);
+
+ // Tell update 7017 that it isn't needed.
+ variable_set('metatag_skip_update_7017', TRUE);
+
+ // Add the new field.
+ db_add_field('metatag', 'revision_id', array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The revision_id for the entity object this data is attached to.',
+ ));
+
+ // Remove the existing primary key. This may take some work so it can be
+ // database agnostic, i.e. some databases will not like it.
+ db_drop_primary_key('metatag');
+
+ // Add the new primary key.
+ db_add_primary_key('metatag', array('entity_type', 'entity_id', 'revision_id', 'language'));
+
+ drupal_set_message(t('Added the {metatag}.revision_id field.'));
+ }
+ else {
+ drupal_set_message(t('The {metatag}.revision_id field has already been added, nothing to do.'));
+ }
+}
+
+/**
+ * Update the revision ID to fix the NULL values, help avoid problems with
+ * update 7017.
+ */
+function metatag_update_7016() {
+ // It's possible that 7015 was not executed if the site had been updated to
+ // an early dev release, so make sure the revision_id field exists.
+ metatag_update_7015();
+
+ // Run the update.
+ db_query("UPDATE {metatag} SET revision_id = 0 WHERE revision_id IS NULL");
+}
+
+/**
+ * The {metatag}.revision_id field is required.
+ */
+function metatag_update_7017() {
+ if (!variable_get('metatag_skip_update_7017', FALSE)) {
+ // Let's add a temporary unique key so MySQL will let it go.
+ db_add_unique_key('metatag', 'temp_key', array('entity_type', 'entity_id', 'revision_id', 'language'));
+
+ // Now remove the PK before changing a field from serial.
+ db_drop_primary_key('metatag');
+
+ // Change the field.
+ db_change_field('metatag', 'revision_id', 'revision_id', array(
'type' => 'int',
'unsigned' => TRUE,
- 'not null' => FALSE,
+ 'not null' => TRUE,
'default' => 0,
'description' => 'The revision_id for the entity object this data is attached to.',
));
- // Remove the existing primary key. This may take some work so it can be
- // database agnostic, i.e. some databases will not like it.
- db_drop_primary_key('metatag');
+ // Manually re-add the PK.
+ db_add_primary_key('metatag', array('entity_type', 'entity_id', 'revision_id', 'language'));
- // Add the new primary key.
- db_add_primary_key('metatag', array('entity_type', 'entity_id', 'revision_id', 'language'));
+ // Finally, remove the temporary unique key because it's no longer useful.
+ db_drop_unique_key('metatag', 'temp_key');
- drupal_set_message(t('Added the {metatag}.revision_id field.'));
+ drupal_set_message(t('Fixed the {metatag}.revision_id field.'));
+ }
+ else {
+ drupal_set_message(t("Didn't need to fix the {metatag}.revision_id field; please disperse, nothing to see here."));
+ }
+
+ // Delete the temporary variable.
+ variable_del('metatag_skip_update_7017');
+}
+
+/**
+ * Update the revision ID for each record. This may take some time. Should any
+ * nodes be discovered with a meta tag record for both revision_id 0 and the
+ * correct revision_id, the "0" value will be deleted; if this is not the
+ * desired result the {metatag} table must be manually pruned to have the
+ * correct records prior to letting this update run.
+ */
+function metatag_update_7018(&$sandbox) {
+ // Process records in small groups.
+ // When a group is processed, the batch update engine determines whether it
+ // should continue processing in the same request or provide progress
+ // feedback to the user and wait for the next request.
+ $limit = 10;
+ // When ran through Drush it's Ok to process a larger number of objects at a
+ // time.
+ if (drupal_is_cli()) {
+ $limit = 100;
+ }
+
+ // Use the sandbox at your convenience to store the information needed
+ // to track progression between successive calls to the function.
+ if (!isset($sandbox['progress'])) {
+ // The count of records visited so far.
+ $sandbox['progress'] = 0;
+
+ // Get a list of all records affected.
+ $sandbox['records'] = db_query("SELECT entity_type, entity_id, language
+ FROM {metatag}
+ WHERE revision_id = 0")
+ ->fetchAll();
+
+ // If there's no data, don't bother with the extra work.
+ if (empty($sandbox['records'])) {
+ watchdog('metatag', 'Update 7018: No {metatag} records needed to have the revision_id value fixed.', array(), WATCHDOG_INFO);
+ if (drupal_is_cli()) {
+ drupal_set_message(t('Update 7018: No {metatag} records needed to have the revision_id value fixed.'));
+ }
+ return t('No {metatag} records needed to have the revision_id value fixed.');
+ }
+
+ // Total records that must be visited.
+ $sandbox['max'] = count($sandbox['records']);
+
+ // A place to store messages during the run.
+ $sandbox['messages'] = array();
+
+ // An initial record of the number of records to be updated.
+ watchdog('metatag', 'Update 7018: !count records to update.', array('!count' => $sandbox['max']), WATCHDOG_INFO);
+ if (drupal_is_cli()) {
+ drupal_set_message(t('Update 7018: !count records to update.', array('!count' => $sandbox['max'])));
+ }
+
+ // Last record processed.
+ $sandbox['current_record'] = -1;
+
+ // Because a lot of other processing happens on the first iteration, just do
+ // one.
+ $limit = 1;
+ }
+
+ // Work out which entities support languages and revisions.
+ $has_language = array();
+ $has_revisions = array();
+ foreach (entity_get_info() as $entity_type => $info) {
+ $has_language[$entity_type] = FALSE;
+ $has_revisions[$entity_type] = FALSE;
+ if (!empty($info['entity keys']['language'])) {
+ $has_language[$entity_type] = $info['entity keys']['language'];
+ }
+ if (!empty($info['entity keys']['revision'])) {
+ $has_revisions[$entity_type] = $info['entity keys']['revision'];
+ }
+ }
+
+ // Set default values.
+ for ($ctr = 0; $ctr < $limit; $ctr++) {
+ $sandbox['current_record']++;
+ if (empty($sandbox['records'][$sandbox['current_record']])) {
+ break;
+ }
+
+ // Shortcuts for later.
+ $entity_type = $sandbox['records'][$sandbox['current_record']]->entity_type;
+ $entity_id = $sandbox['records'][$sandbox['current_record']]->entity_id;
+ // Make sure to load the correct language record.
+ $language = $sandbox['records'][$sandbox['current_record']]->language;
+ $conditions = array();
+ // Some entities don't include a language value.
+ if (!empty($has_language[$entity_type])) {
+ $conditions['language'] = $language;
+ }
+ $records = entity_load($entity_type, array($entity_id), $conditions);
+
+ // Fix this record.
+ if (!empty($records)) {
+ $entity = reset($records);
+
+ // Speed up the handling of entities that don't support revisions.
+ $revision_id = 0;
+ if (!empty($has_revisions[$entity_type])) {
+ list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
+ }
+
+ // Don't bother updating records if the revision_id is 0.
+ if (!empty($revision_id)) {
+ $exists = db_query("SELECT entity_id
+ FROM {metatag}
+ WHERE entity_type = :entity_type
+ AND entity_id = :entity_id
+ AND revision_id = :revision_id
+ AND language = :language",
+ array(
+ ':entity_type' => $entity_type,
+ ':entity_id' => $entity_id,
+ ':revision_id' => $revision_id,
+ ':language' => $language,
+ ))->fetchObject();
+ // There isn't already a record for the revision_id, so update the
+ // metatag record.
+ if (!$exists) {
+ db_update('metatag')
+ ->fields(array('revision_id' => $revision_id))
+ ->condition('entity_type', $entity_type)
+ ->condition('entity_id', $entity_id)
+ ->condition('revision_id', 0)
+ ->condition('language', $language)
+ ->execute();
+ }
+ // The record exists, so delete the old one under the grounds that the
+ // one with a revision_id is newer.
+ // Disclaimer: this is completely arbitrary, without providing a UI to
+ // let the site maintainer/builder choose which of the two records to
+ // keep, we're stuck with a bad scenario. Thankfully this should not
+ // happen very often and would only affect sites that were running a
+ // dev release. Also, sorry :(
+ else {
+ db_delete('metatag')
+ ->condition('entity_type', $entity_type)
+ ->condition('entity_id', $entity_id)
+ ->condition('revision_id', 0)
+ ->condition('language', $language)
+ ->execute();
+ }
+
+ // Nodes can have multiple revisions, so create new {metatag} records
+ // for each of the other revisions.
+ if ($entity_type == 'node') {
+ $revisions = node_revision_list($entity);
+ if (count($revisions) > 1) {
+ $metatags = db_query("SELECT data
+ FROM {metatag}
+ WHERE entity_type = :entity_type
+ AND entity_id = :entity_id
+ AND language = :language",
+ array(
+ ':entity_type' => $entity_type,
+ ':entity_id' => $entity_id,
+ ':language' => $language,
+ ));
+ if (!empty($metatags) && isset($metatags->data) && !empty($metatags->data)) {
+ foreach ($revisions as $vid => $revision) {
+ // Only one record per nid/vid/langcode, thank you.
+ if ($vid != $revision_id) {
+ // Check that there isn't already a record for this revision.
+ $exists = db_query("SELECT entity_id
+ FROM {metatag}
+ WHERE entity_type = :entity_type
+ AND entity_id = :entity_id
+ AND revision_id = :revision_id
+ AND language = :language",
+ array(
+ ':entity_type' => $entity_type,
+ ':entity_id' => $entity_id,
+ ':revision_id' => $vid,
+ ':language' => $language,
+ ))->fetchObject();
+ if (!$exists) {
+ $node = node_load($entity_id, $vid);
+ $record = new StdClass();
+ $record->entity_type = $entity_type;
+ $record->entity_id = $entity_id;
+ $record->revision_id = $vid;
+ $record->language = $language;
+ $record->data = $metatags->data;
+ drupal_write_record('metatag', $record);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Other entity types.
+ else {
+ drupal_set_message(t('Metatag records for @type objects have not been checked for revisions.', array('@type' => $entity_type)), 'status', FALSE);
+ }
+ }
+ }
+
+ // Update our progress information.
+ $sandbox['progress']++;
+ }
+
+ // Set the "finished" status, to tell batch engine whether this function
+ // needs to run again. If you set a float, this will indicate the progress of
+ // the batch so the progress bar will update.
+ $sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']);
+
+ if ($sandbox['#finished']) {
+ // Clear all caches so the fixed data will be reloaded.
+ cache_clear_all('*', 'cache_metatag', TRUE);
+
+ // A final log of the number of records that were converted.
+ watchdog('metatag', 'Update 7018: !count records were updated in total.', array('!count' => $sandbox['progress']), WATCHDOG_INFO);
+ if (drupal_is_cli()) {
+ drupal_set_message(t('Update 7018: !count records were updated.', array('!count' => $sandbox['progress'])));
+ }
+
+ // hook_update_N() may optionally return a string which will be displayed
+ // to the user.
+ return t('Fixed the revision_id values for !count {metatag} records.', array('!count' => $sandbox['progress']));
+ }
+}
+
+/**
+ * Clear the entity_cache bins.
+ */
+function metatag_update_7019() {
+ if (module_exists('entitycache')) {
+ foreach (drupal_get_schema() as $table_name => $spec) {
+ if (strpos($table_name, 'cache_entity_') === 0) {
+ cache_clear_all('*', $table_name, TRUE);
+ drupal_set_message(t("Cleared the @table cache bin", array('@table' => $table_name)));
+ }
+ }
+ }
+ else {
+ drupal_set_message(t("The EntityCache module is not installed, nothing to do."));
+ }
+}
+
+/**
+ * Clear the Metatag cache.
+ */
+function metatag_update_7020() {
+ cache_clear_all('*', 'cache_metatag', TRUE);
+ return t('All Metatag caches cleared.');
+}
+
+/**
+ * Clear the existing Metatag cache so all unwanted 403/404 paths can be
+ * purged.
+ */
+function metatag_update_7021() {
+ cache_clear_all('*', 'cache_metatag', TRUE);
+ return t('All Metatag caches cleared.');
+}
+
+/**
+ * A minor bit of tidy-up after update 7015.
+ */
+function metatag_update_7022() {
+ variable_del('metatag_skip_update_7015');
+}
+
+/**
+ * Clear the Metatag cache because $cid_parts was changed.
+ */
+function metatag_update_7023() {
+ cache_clear_all('*', 'cache_metatag', TRUE);
+ return t('All Metatag caches cleared.');
+}
+
+/**
+ * Rename the 'twitter:image' meta tag to 'twitter:image:src', part 1.
+ */
+function metatag_update_7024() {
+ // Find all {metatag} records that contained an entry for the old meta tag.
+ $records = db_query("SELECT entity_type, entity_id, revision_id, language, data
+ FROM {metatag}
+ WHERE data LIKE '%twitter:image%'");
+ // This message will be returned if nothing needed to be updated.
+ $none_message = t('No Metatag entity records needed to have the "twitter:image" meta tag fixed.');
+
+ if ($records->rowCount() == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ $keys = array('entity_type', 'entity_id', 'revision_id', 'language');
+
+ // Loop over the values and correct them.
+ $counter = 0;
+ foreach ($records as $record) {
+ $record->data = unserialize($record->data);
+ if (isset($record->data['twitter:image'])) {
+ $record->data['twitter:image:src'] = $record->data['twitter:image'];
+ unset($record->data['twitter:image']);
+ drupal_write_record('metatag', $record, $keys);
+ $counter++;
+ }
+ }
+ if ($counter == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ drupal_set_message(t('Converted the "twitter:image" meta tag for @count entity records to the correct "twitter:image:src" meta tag.', array('@count' => $counter)));
+ }
+ }
+}
+
+/**
+ * Replaced by update 7030.
+ */
+function metatag_update_7025() {
+ // Do nothing.
+}
+
+/**
+ * Rename the 'copyright' meta tag to 'rights', part 1.
+ */
+function metatag_update_7026() {
+ // Find all {metatag} records that contained an entry for the old meta tag.
+ $records = db_query("SELECT entity_type, entity_id, revision_id, language, data
+ FROM {metatag}
+ WHERE data LIKE '%copyright%'");
+ // This message will be returned if nothing needed to be updated.
+ $none_message = t('No Metatag entity records needed to have the "copyright" meta tag fixed.');
+
+ if ($records->rowCount() == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ $keys = array('entity_type', 'entity_id', 'revision_id', 'language');
+
+ // Loop over the values and correct them.
+ $counter = 0;
+ foreach ($records as $record) {
+ $record->data = unserialize($record->data);
+ if (isset($record->data['copyright'])) {
+ $record->data['rights'] = $record->data['copyright'];
+ unset($record->data['copyright']);
+ drupal_write_record('metatag', $record, $keys);
+ $counter++;
+ }
+ }
+ if ($counter == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ drupal_set_message(t('Converted the "copyright" meta tag for @count entity records to the correct "rights" meta tag.', array('@count' => $counter)));
+ }
+ }
+}
+
+/**
+ * Replaced by update 7031.
+ */
+function metatag_update_7027() {
+ // Do nothing.
+}
+
+/**
+ * Clear the menu cache so the new Advanced Settings page will be picked up.
+ */
+function metatag_update_7028() {
+ variable_set('menu_rebuild_needed', TRUE);
+}
+
+/**
+ * Add an index to the {metatag} table to speed up some queries.
+ */
+function metatag_update_7029() {
+ db_add_index('metatag', 'type_revision', array('entity_type', 'revision_id'));
+ drupal_set_message(t('Added an index to the main Metatag table that will hopefully improve performance a little.'));
+}
+
+/**
+ * Rename the 'twitter:image' meta tag to 'twitter:image:src', part 2.
+ */
+function metatag_update_7030() {
+ // Find all {metatag_config} records that contained an entry for the old meta
+ // tag.
+ $records = db_query("SELECT cid, config
+ FROM {metatag_config}
+ WHERE config LIKE '%twitter:image%'");
+ // This message will be returned if nothing needed to be updated.
+ $none_message = t('No Metatag configuration records needed to have the "twitter:image" meta tag fixed. That said, there may be other configurations elsewhere that do need updating.');
+
+ // Loop over the values and correct them.
+ if ($records->rowCount() == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ $keys = array('cid');
+
+ // Loop over the values and correct them.
+ $counter = 0;
+ foreach ($records as $record) {
+ $record->config = unserialize($record->config);
+ if (isset($record->config['twitter:image'])) {
+ $record->config['twitter:image:src'] = $record->config['twitter:image'];
+ unset($record->config['twitter:image']);
+ drupal_write_record('metatag_config', $record, $keys);
+ $counter++;
+ }
+ }
+ if ($counter == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ drupal_set_message(t('Converted the "twitter:image" meta tag for @count configurations to the correct "twitter:image:src" meta tag.', array('@count' => $counter)));
+ }
+ }
+}
+
+/**
+ * Rename the 'copyright' meta tag to 'rights', part 2.
+ */
+function metatag_update_7031() {
+ // Find all {metatag_config} records that contained an entry for the old meta
+ // tag.
+ $records = db_query("SELECT cid, config
+ FROM {metatag_config}
+ WHERE config LIKE '%copyright%'");
+ // This message will be returned if nothing needed to be updated.
+ $none_message = t('No Metatag configuration records needed to have the "copyright" meta tag fixed. That said, there may be other configurations elsewhere that do need updating.');
+
+ // Loop over the values and correct them.
+ if ($records->rowCount() == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ $keys = array('cid');
+
+ // Loop over the values and correct them.
+ $counter = 0;
+ foreach ($records as $record) {
+ $record->config = unserialize($record->config);
+ if (isset($record->config['copyright'])) {
+ $record->config['rights'] = $record->config['copyright'];
+ unset($record->config['copyright']);
+ drupal_write_record('metatag_config', $record, $keys);
+ $counter++;
+ }
+ }
+ if ($counter == 0) {
+ drupal_set_message($none_message);
+ }
+ else {
+ drupal_set_message(t('Converted the "copyright" meta tag for @count configurations to the correct "rights" meta tag.', array('@count' => $counter)));
+ }
+ }
+}
+
+/**
+ * Clear the Metatag cache.
+ */
+function metatag_update_7032() {
+ cache_clear_all('*', 'cache_metatag', TRUE);
+ return t('All Metatag caches cleared.');
+}
+
+/**
+ * These originally removed the 'author' meta tag, but it was subsequently
+ * decided that this was not the correct approach, that the meta tag should not
+ * be removed after all.
+ *
+ * @see https://www.drupal.org/node/2330823
+ */
+function metatag_update_7033() {
+}
+function metatag_update_7034() {
+}
+function metatag_update_7035() {
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.metatag.inc b/sites/all/modules/contrib/seo/metatag/metatag.metatag.inc
index 253ded99..340960a7 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.metatag.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.metatag.inc
@@ -29,6 +29,26 @@ function metatag_metatag_config_default() {
);
$configs[$config->instance] = $config;
+ $config = new stdClass();
+ $config->instance = 'global:403';
+ $config->api_version = 1;
+ $config->disabled = FALSE;
+ $config->config = array(
+ 'canonical' => array('value' => '[site:url]'),
+ 'shortlink' => array('value' => '[site:url]'),
+ );
+ $configs[$config->instance] = $config;
+
+ $config = new stdClass();
+ $config->instance = 'global:404';
+ $config->api_version = 1;
+ $config->disabled = FALSE;
+ $config->config = array(
+ 'canonical' => array('value' => '[site:url]'),
+ 'shortlink' => array('value' => '[site:url]'),
+ );
+ $configs[$config->instance] = $config;
+
$config = new stdClass();
$config->instance = 'node';
$config->api_version = 1;
@@ -60,6 +80,12 @@ function metatag_metatag_config_default() {
);
$configs[$config->instance] = $config;
+ // Before returning these, allow the bundled submodules to override them, thus
+ // extending the "real" defaults before they can then be altered by other
+ // modules.
+ // See hook_metatag_bundled_config_alter() in the API documentation.
+ drupal_alter('metatag_bundled_config', $configs);
+
return $configs;
}
@@ -69,9 +95,8 @@ function metatag_metatag_config_default() {
function metatag_metatag_config_instance_info() {
$info['global'] = array('label' => t('Global'));
$info['global:frontpage'] = array('label' => t('Front page'));
- // @todo The 403 and 404 meta tag contexts are disabled until they can be properly implemented.
- //$info['global:403'] = array('label' => t('403 page not found'));
- //$info['global:404'] = array('label' => t('404 page not found'));
+ $info['global:403'] = array('label' => t('403 access denied'));
+ $info['global:404'] = array('label' => t('404 page not found'));
// Add instance information for entities.
$entity_types = entity_get_info();
@@ -104,16 +129,21 @@ function metatag_metatag_info() {
),
);
+ // "Simple" meta tags go first.
+ $weight = 0;
+
$info['tags']['title'] = array(
'label' => t('Page title'),
'description' => t("The text to display in the title bar of a visitor's web browser when they view this page. This meta tag may also be used as the title of the page when a visitor bookmarks or favorites this page."),
'class' => 'DrupalTitleMetaTag',
+ 'weight' => ++$weight,
);
$info['tags']['description'] = array(
'label' => t('Description'),
'description' => t("A brief and concise summary of the page's content, preferably 150 characters or less. The description meta tag may be used by search engines to display a snippet about the page in search results."),
'class' => 'DrupalTextMetaTag',
+ 'weight' => ++$weight,
'form' => array(
'#type' => 'textarea',
'#rows' => 2,
@@ -125,6 +155,7 @@ function metatag_metatag_info() {
'label' => t('Abstract'),
'description' => t("A brief and concise summary of the page's content, preferably 150 characters or less. The abstract meta tag may be used by search engines for archiving purposes."),
'class' => 'DrupalTextMetaTag',
+ 'weight' => ++$weight,
'form' => array(
'#type' => 'textarea',
'#rows' => 2,
@@ -134,8 +165,9 @@ function metatag_metatag_info() {
$info['tags']['keywords'] = array(
'label' => t('Keywords'),
- 'description' => t("A comma-separated list of keywords about the page. This meta tag is not used by most search engines."),
+ 'description' => t("A comma-separated list of keywords about the page. This meta tag is not supported by most search engines."),
'class' => 'DrupalTextMetaTag',
+ 'weight' => ++$weight,
);
// More advanced meta tags.
@@ -143,19 +175,22 @@ function metatag_metatag_info() {
'label' => t('Robots'),
'description' => t("Provides search engines with specific directions for what to do when this page is indexed."),
'class' => 'DrupalListMetaTag',
+ 'group' => 'advanced',
+ 'weight' => ++$weight,
'form' => array(
'#options' => array(
'index' => t('Allow search engines to index this page (assumed).'),
'follow' => t('Allow search engines to follow links on this page (assumed).'),
- 'noindex' => t('Prevent search engines from indexing this page.'),
- 'nofollow' => t('Prevent search engines from following links on this page.'),
- 'noarchive' => t('Prevent a cached copy of this page from being available in the search results.'),
- 'nosnippet' => t('Prevents a description from appearing below the page in the search results, as well as prevents caching of the page.'),
- 'noodp' => t('Blocks the Open Directory Project description of the page from being used in the description that appears below the page in the search results.', array('@odp-url' => 'http://www.dmoz.org/')),
+ 'noindex' => t('Prevents search engines from indexing this page.'),
+ 'nofollow' => t('Prevents search engines from following links on this page.'),
+ 'noarchive' => t('Prevents cached copies of this page from appearing in search results.'),
+ 'nosnippet' => t('Prevents descriptions from appearing in search results, and prevents page caching.'),
+ 'noodp' => t('Blocks the !opendirectory description from appearing in search results.', array('!opendirectory' => l('Open Directory Project', 'http://www.dmoz.org/'))),
'noydir' => t('Prevents Yahoo! from listing this page in the Yahoo! Directory.', array('@ydir' => 'http://dir.yahoo.com/')),
+ 'noimageindex' => t('Prevent search engines from indexing images on this page.'),
+ 'notranslate' => t('Prevent search engines from offering to translation this page in search results.'),
),
),
- 'group' => 'advanced',
);
$info['tags']['news_keywords'] = array(
@@ -163,13 +198,15 @@ function metatag_metatag_info() {
'description' => t('A comma-separated list of keywords about the page. This meta tag is used as an indicator in Google News.', array('@google_news' => 'http://support.google.com/news/publisher/bin/answer.py?hl=en&answer=68297')),
'class' => 'DrupalTextMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
);
-
+
$info['tags']['standout'] = array(
'label' => t('Google Standout'),
'description' => t("Highlight standout journalism on the web, especially for breaking news; used as an indicator in Google News. Warning: Don't abuse it, to be used a maximum of 7 times per calendar week!", array('@google_news' => 'https://support.google.com/news/publisher/answer/191283?hl=en&ref_topic=2484650')),
'class' => 'DrupalTextMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
);
$info['tags']['generator'] = array(
@@ -179,13 +216,18 @@ function metatag_metatag_info() {
'header' => 'X-Generator',
'context' => array('global'),
'group' => 'advanced',
+ 'weight' => ++$weight,
);
- $info['tags']['copyright'] = array(
- 'label' => t('Copyright'),
- 'description' => t("Details a copyright, trademark, patent, or other information that pertains to intellectual property about this page. Note that this will not automatically protect your site's content or your intellectual property."),
+ $info['tags']['rights'] = array(
+ 'label' => t('Rights'),
+ 'description' => t("Details about intellectual property, such as copyright or trademarks; does not automatically protect the site's content or intellectual property."),
'class' => 'DrupalTextMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'replaces' => array(
+ 'copyright',
+ ),
);
$info['tags']['image_src'] = array(
@@ -193,20 +235,32 @@ function metatag_metatag_info() {
'description' => t("An image associated with this page, for use as a thumbnail in social networks and other services."),
'class' => 'DrupalLinkMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
);
$info['tags']['canonical'] = array(
'label' => t('Canonical URL'),
- 'description' => t("Tells search engines where the preferred location or URL is for this page to help eliminate self-created duplicate content for search engines."),
+ 'description' => t("Preferred page location or URL to help eliminate duplicate content for search engines."),
'class' => 'DrupalLinkMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'canonical',
+ ),
);
$info['tags']['shortlink'] = array(
'label' => t('Shortlink URL'),
- 'description' => '',
+ 'description' => t('A brief URL, often created by a URL shortening service.'),
'class' => 'DrupalLinkMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'shortlink',
+ ),
);
$info['tags']['publisher'] = array(
@@ -214,6 +268,10 @@ function metatag_metatag_info() {
'description' => '',
'class' => 'DrupalLinkMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
);
$info['tags']['author'] = array(
@@ -221,6 +279,10 @@ function metatag_metatag_info() {
'description' => t("Used by some search engines to confirm authorship of the content on a page. Should be either the full URL for the author's Google+ profile page or a local page with information about the author."),
'class' => 'DrupalLinkMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
);
$info['tags']['original-source'] = array(
@@ -229,6 +291,10 @@ function metatag_metatag_info() {
'class' => 'DrupalTextMetaTag',
'group' => 'advanced',
'description' => t("Used to indicate the URL that broke the story, and can link to either an internal URL or an external source. If the full URL is not known it is acceptable to use a partial URL or just the domain name."),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
);
$info['tags']['revisit-after'] = array(
@@ -236,6 +302,21 @@ function metatag_metatag_info() {
'description' => t('Tell search engines when to index the page again. Very few search engines support this tag, it is more useful to use an XML Sitemap file.', array('@xmlsitemap' => 'http://drupal.org/project/xmlsitemap')),
'class' => 'DrupalDateIntervalMetaTag',
'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'date',
+ ),
+ );
+
+ $info['tags']['content-language'] = array(
+ 'label' => t('Content language'),
+ 'description' => t("A deprecated meta tag for defining this page's two-letter language code(s)."),
+ 'class' => 'DrupalTextMetaTag',
+ 'group' => 'advanced',
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_http_equiv',
+ ),
);
return $info;
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.migrate.inc b/sites/all/modules/contrib/seo/metatag/metatag.migrate.inc
index efa603a2..a4ef0105 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.migrate.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.migrate.inc
@@ -82,14 +82,14 @@ function metatag_migrate_api() {
class MigrateMetatagHandler extends MigrateDestinationHandler {
public function __construct() {
- $types = array();
+ $entity_types = array();
foreach (entity_get_info() as $entity_type => $entity_info) {
if (isset($entity_info['metatags']) && $entity_info['metatags']) {
- $types[] = $entity_type;
+ $entity_types[] = $entity_type;
}
}
- $this->registerTypes($types);
+ $this->registerTypes($entity_types);
}
/**
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.module b/sites/all/modules/contrib/seo/metatag/metatag.module
index 23579fa5..7f2edd6e 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.module
+++ b/sites/all/modules/contrib/seo/metatag/metatag.module
@@ -17,7 +17,6 @@ function metatag_help($path, $arg) {
elseif ($path == 'admin/config/search/metatags/bulk-revert') {
return '' . t('This form will wipe out all custom meta tags for the selected entities, reverting them to the default configuration assigned at the Defaults tab. For example, if the meta tags are changed for an article they will be removed if the "Node: Article" checkbox is selected.', array('@url' => url('admin/config/search/metatags'))) . '
';
}
-
}
/**
@@ -40,6 +39,10 @@ function metatag_theme() {
'render element' => 'element',
'file' => 'metatag.theme.inc',
);
+ $info['metatag_property'] = array(
+ 'render element' => 'element',
+ 'file' => 'metatag.theme.inc',
+ );
return $info;
}
@@ -182,13 +185,22 @@ function metatag_menu() {
'type' => MENU_LOCAL_TASK,
'weight' => 10,
);
+ $items['admin/config/search/metatags/settings'] = array(
+ 'title' => 'Advanced settings',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('metatag_admin_settings_form'),
+ 'access arguments' => array('administer meta tags'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 30,
+ 'file' => 'metatag.admin.inc',
+ );
$items['admin/config/search/metatags/bulk-revert'] = array(
'title' => 'Bulk revert',
'page callback' => 'drupal_get_form',
'page arguments' => array('metatag_bulk_revert_form'),
'access arguments' => array('administer meta tags'),
'type' => MENU_LOCAL_TASK,
- 'weight' => 30,
+ 'weight' => 40,
'file' => 'metatag.admin.inc',
);
@@ -218,7 +230,7 @@ function metatag_config_load_with_defaults($instance, $include_global = TRUE) {
// Statically cache defaults since they can include multiple levels.
$cid = "config:{$instance}" . ($include_global ? ':withglobal' : ':withoutglobal');
if (!isset($defaults[$cid])) {
- if ($cache = cache_get($cid, 'cache_metatag')) {
+ if ($cache = metatag_cache_get($cid)) {
$defaults[$cid] = $cache->data;
}
else {
@@ -232,10 +244,12 @@ function metatag_config_load_with_defaults($instance, $include_global = TRUE) {
}
// Add config to the defaults array.
- $defaults[$cid] += $configs[$key]->config;
+ if (!empty($configs[$key]->config)) {
+ $defaults[$cid] += $configs[$key]->config;
+ }
}
- cache_set($cid, $defaults[$cid], 'cache_metatag');
+ metatag_cache_set($cid, $defaults[$cid]);
}
}
@@ -254,8 +268,46 @@ function metatag_config_load($instance) {
* Load multiple metatag configuration records.
*/
function metatag_config_load_multiple(array $instances) {
+ // Load the data.
ctools_include('export');
- return ctools_export_load_object('metatag_config', 'names', $instances);
+ $configs = ctools_export_load_object('metatag_config', 'names', $instances);
+
+ // "Fix" any records that might be using old values. Ideally these will be
+ // permanently fixed by being re-saved or re-exported.
+ foreach (metatag_config_get_replacements() as $old_tag => $new_tag) {
+ foreach ($configs as $config_name => $config) {
+ if (isset($config->config[$old_tag])) {
+ $config->config[$new_tag] = $config->config[$old_tag];
+ unset($config->config[$old_tag]);
+ }
+ }
+ }
+
+ return $configs;
+}
+
+/**
+ * Identify the meta tags that have been deprecated and replaced by others.
+ */
+function metatag_config_get_replacements() {
+ $replacements = &drupal_static(__FUNCTION__);
+
+ if (!isset($replacements)) {
+ $replacements = array();
+
+ foreach (metatag_get_info('tags') as $tag_name => $tag_info) {
+ if (!empty($tag_info['replaces'])) {
+ if (!is_array($tag_info['replaces'])) {
+ $tag_info['replaces'] = array($tag_info['replaces']);
+ }
+ foreach ($tag_info['replaces'] as $replaces) {
+ $replacements[$replaces] = $tag_name;
+ }
+ }
+ }
+ }
+
+ return $replacements;
}
/**
@@ -327,11 +379,12 @@ function metatag_config_cache_clear() {
* Load an entity's tags.
*
* @param $entity_type
- * The entity type to load
+ * The entity type to load.
* @param $entity_id
- * The ID of the entity to load
+ * The ID of the entity to load.
+ *
* @return
- * An array of tag data keyed by language.
+ * An array of tag data keyed by revision ID and language.
*/
function metatag_metatags_load($entity_type, $entity_id) {
$metatags = metatag_metatags_load_multiple($entity_type, array($entity_id));
@@ -342,13 +395,14 @@ function metatag_metatags_load($entity_type, $entity_id) {
* Load tags for multiple entities.
*
* @param $entity_type
- * The entity type to load
+ * The entity type to load.
* @param $entity_ids
- * The list of entity IDs
+ * The list of entity IDs.
+ *
* @return
- * An array of tag data, keyed by ID.
+ * An array of tag data, keyed by entity ID, revision ID and language.
*/
-function metatag_metatags_load_multiple($entity_type, array $entity_ids, array $revision_ids) {
+function metatag_metatags_load_multiple($entity_type, array $entity_ids, array $revision_ids = array()) {
// Double check entity IDs are numeric thanks to Entity API module.
$entity_ids = array_filter($entity_ids, 'is_numeric');
if (empty($entity_ids)) {
@@ -367,18 +421,49 @@ function metatag_metatags_load_multiple($entity_type, array $entity_ids, array $
}
}
+ // Verify that the metatag.revision_id field has been added to the {metatag}
+ // table schema.
+ if (!variable_get('metatag_has_revision_id', FALSE)) {
+ if (db_field_exists('metatag', 'revision_id')) {
+ variable_set('metatag_has_revision_id', TRUE);
+ }
+ else {
+ watchdog('metatag', 'The database updates need to be ran.', array(), WATCHDOG_WARNING);
+ return array();
+ }
+ }
+
// Get all translations of tag data for this entity.
- $result = db_query("SELECT entity_id, data, language FROM {metatag} WHERE (entity_type = :type) AND (entity_id IN (:ids)) AND (revision_id IN (:vids))", array(
- ':type' => $entity_type,
- ':ids' => $entity_ids,
- ':vids' => $revision_ids,
- ));
+ $query = db_select('metatag', 'm')
+ ->fields('m', array('entity_id', 'revision_id', 'language', 'data'))
+ ->condition('m.entity_type', $entity_type)
+ ->orderBy('entity_id')
+ ->orderBy('revision_id');
+ // Filter by revision_ids if they are available. If not, filter by entity_ids.
+ if (!empty($revision_ids)) {
+ $query->condition('m.revision_id', $revision_ids, 'IN');
+ }
+ else {
+ $query->condition('m.entity_id', $entity_ids, 'IN');
+ }
+ $result = $query->execute();
// Marshal it into an array keyed by entity ID. Each value is an array of
// translations keyed by language code.
$metatags = array();
while ($record = $result->fetchObject()) {
- $metatags[$record->entity_id][$record->language] = unserialize($record->data);
+ $data = unserialize($record->data);
+
+ // "Fix" any records that might be using old values. Ideally these will be
+ // permanently fixed by being re-saved or re-exported.
+ foreach (metatag_config_get_replacements() as $old_tag => $new_tag) {
+ if (isset($data[$old_tag])) {
+ $data[$new_tag] = $data[$old_tag];
+ unset($data[$old_tag]);
+ }
+ }
+
+ $metatags[$record->entity_id][$record->revision_id][$record->language] = $data;
}
return $metatags;
@@ -398,9 +483,11 @@ function metatag_metatags_load_multiple($entity_type, array $entity_ids, array $
* @param $language
* The language of the translation set
*/
-function metatag_metatags_save($entity_type, $entity_id, $revision_id, $metatags, $langcode) {
- // If no language assigned, use the has-no-language language.
- if (empty($langcode)) {
+function metatag_metatags_save($entity_type, $entity_id, $revision_id, $metatags, $langcode, $old_vid = NULL) {
+ // If no language assigned, or the language doesn't exist, use the
+ // has-no-language language.
+ $languages = language_list();
+ if (empty($langcode) || !isset($languages[$langcode])) {
$langcode = LANGUAGE_NONE;
}
@@ -409,52 +496,64 @@ function metatag_metatags_save($entity_type, $entity_id, $revision_id, $metatags
return;
}
- // If the vid was not passed in, use the entity_id.
- if (empty($revision_id)) {
- $revision_id = $entity_id;
+ // The revision_id must be a numeric value; some entities use NULL for the
+ // revision so change that to a zero.
+ if (is_null($revision_id)) {
+ $revision_id = 0;
}
- // Ensure the data saves during node_save().
- if (isset($metatags[$langcode])) {
- // There are certain occasions when the old data and the new data are
- // *both* added to the $metatags array, in this case throw away the language
- // data.
- $lang_data = $metatags[$langcode];
- unset($metatags[$langcode]);
- if (empty($metatags)) {
- $metatags = $lang_data;
+ // Handle scenarios where the metatags are completely empty.
+ if (empty($metatags)) {
+ $metatags = array();
+ // Add an empty array record for each language.
+ $languages = db_query("SELECT language, ''
+ FROM {metatag}
+ WHERE (entity_type = :type)
+ AND (entity_id = :id)
+ AND (revision_id = :revision)",
+ array(
+ ':type' => $entity_type,
+ ':id' => $entity_id,
+ ':revision' => $revision_id,
+ ))->fetchAllKeyed();
+ foreach ($languages as $oldlang => $empty) {
+ $metatags[$oldlang] = array();
}
}
- // Allow other modules to alter the meta tags prior to saving using
- // hook_metatag_presave().
- foreach (module_implements('metatag_presave') as $module) {
- $function = "{$module}_metatag_presave";
- $function($metatags, $entity_type, $entity_id, $revision_id, $langcode);
- }
+ // Update each of the per-language metatag configurations in turn.
+ foreach ($metatags as $langcode => $new_metatags) {
+ // Allow other modules to alter the meta tags prior to saving using
+ // hook_metatag_presave().
+ foreach (module_implements('metatag_presave') as $module) {
+ $function = "{$module}_metatag_presave";
+ $function($new_metatags, $entity_type, $entity_id, $revision_id, $langcode);
+ }
- if (empty($metatags)) {
- // If the data array is empty, there is no data to actually save, so
- // just delete the record from the database.
- db_delete('metatag')
- ->condition('entity_type', $entity_type)
- ->condition('entity_id', $entity_id)
- ->condition('language', $langcode)
- ->execute();
- }
- else {
+ // If the data array is empty, there is no data to actually save, so just
+ // delete the record from the database.
+ if (empty($new_metatags)) {
+ db_delete('metatag')
+ ->condition('entity_type', $entity_type)
+ ->condition('entity_id', $entity_id)
+ ->condition('revision_id', $revision_id)
+ ->condition('language', $langcode)
+ ->execute();
+ }
// Otherwise save the data for this entity.
- db_merge('metatag')
- ->key(array(
- 'entity_type' => $entity_type,
- 'entity_id' => $entity_id,
- 'language' => $langcode,
- 'revision_id' => $revision_id,
- ))
- ->fields(array(
- 'data' => serialize($metatags),
- ))
- ->execute();
+ else {
+ db_merge('metatag')
+ ->key(array(
+ 'entity_type' => $entity_type,
+ 'entity_id' => $entity_id,
+ 'language' => $langcode,
+ 'revision_id' => $revision_id,
+ ))
+ ->fields(array(
+ 'data' => serialize($new_metatags),
+ ))
+ ->execute();
+ }
}
// Clear cached data.
@@ -475,7 +574,11 @@ function metatag_metatags_save($entity_type, $entity_id, $revision_id, $metatags
* entries for this entity will be deleted.
*/
function metatag_metatags_delete($entity_type, $entity_id, $revision_id = NULL, $langcode = NULL) {
- return metatag_metatags_delete_multiple($entity_type, array($entity_id), array($revision_id), $langcode);
+ $revision_ids = array();
+ if (!empty($revision_id)) {
+ $revision_ids[] = $revision_id;
+ }
+ return metatag_metatags_delete_multiple($entity_type, array($entity_id), $revision_ids, $langcode);
}
/**
@@ -491,7 +594,7 @@ function metatag_metatags_delete($entity_type, $entity_id, $revision_id = NULL,
* The language ID of the entities to delete. If left blank, all language
* entries for the enities will be deleted.
*/
-function metatag_metatags_delete_multiple($entity_type, array $entity_ids, array $revision_ids, $langcode = NULL) {
+function metatag_metatags_delete_multiple($entity_type, array $entity_ids, array $revision_ids = array(), $langcode = NULL) {
// Double check entity IDs are numeric thanks to Entity API module.
$entity_ids = array_filter($entity_ids, 'is_numeric');
@@ -531,12 +634,20 @@ function metatag_metatags_delete_multiple($entity_type, array $entity_ids, array
}
}
-function metatag_metatags_cache_clear($entity_type, $entity_id = NULL) {
- if (empty($entity_id)) {
+/**
+ * Clear the cached records for a given entity type or entity ID.
+ *
+ * @param string $entity_type
+ * The entity type to clear.
+ */
+function metatag_metatags_cache_clear($entity_type, $entity_ids = NULL) {
+ if (empty($entity_ids)) {
cache_clear_all("output:$entity_type", 'cache_metatag', TRUE);
}
else {
- $entity_ids = (array) $entity_id;
+ if (!is_array($entity_ids)) {
+ $entity_ids = array($entity_ids);
+ }
foreach ($entity_ids as $entity_id) {
cache_clear_all("output:$entity_type:$entity_id", 'cache_metatag', TRUE);
}
@@ -547,30 +658,38 @@ function metatag_metatags_cache_clear($entity_type, $entity_id = NULL) {
* Implements hook_entity_load().
*/
function metatag_entity_load($entities, $entity_type) {
- // get the revision_ids
+ // Get the revision_ids.
$revision_ids = array();
- //since some entities do not have revisions, set the vid to the id
foreach ($entities as $key => $entity) {
list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
- if (!$revision_id) {
- $revision_id = $entity_id;
+ $revision_id = intval($revision_id);
+ if (!empty($revision_id)) {
+ $revision_ids[] = $revision_id;
}
- $revision_ids[] = $revision_id;
}
+
// Wrap this in a try-catch block to work around occasions when the schema
// hasn't been updated yet.
try {
if (metatag_entity_supports_metatags($entity_type)) {
$metatags = metatag_metatags_load_multiple($entity_type, array_keys($entities), $revision_ids);
+
+ // Assign the metatag records for the correct revision ID.
foreach ($entities as $entity_id => $entity) {
- $entities[$entity_id]->metatags = isset($metatags[$entity_id]) ? $metatags[$entity_id] : array();
+ list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
+ $entities[$entity_id]->metatags = isset($metatags[$entity_id][$revision_id]) ? $metatags[$entity_id][$revision_id] : array();
}
}
}
catch (Exception $e) {
- watchdog('metatag', 'Error loading meta tag data, do the database updates need to be run? The error occurred when loading record(s) %ids for the %type entity type. The error message was: %error', array('@update' => base_path() . 'update.php', '%ids' => implode(', ', array_keys($entities)), '%type' => $entity_type, '%error' => $e->getMessage()), WATCHDOG_CRITICAL);
+ watchdog('metatag', 'Error loading meta tag data, do the database updates need to be run? The error occurred when loading record(s) %ids for the %type entity type. The error message was: %error', array('@update' => base_path() . 'update.php', '%ids' => implode(', ', array_keys($entities)), '%type' => $entity_type, '%error' => $e->getMessage()), WATCHDOG_WARNING);
// Don't display the same message twice for Drush.
- if (php_sapi_name() != 'cli') {
+ if (drupal_is_cli()) {
+ drupal_set_message(t('Run your updates, like drush updb.'));
+ }
+ // Only message people who can see it in watchdog and can likely fix it.
+ elseif (user_access('access site reports')) {
drupal_set_message(t('Error loading meta tag data, do the database updates need to be run?', array('@update' => base_path() . 'update.php')), 'error');
}
}
@@ -582,6 +701,7 @@ function metatag_entity_load($entities, $entity_type) {
function metatag_entity_insert($entity, $entity_type) {
if (isset($entity->metatags)) {
list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
// Determine the entity's language.
$langcode = entity_language($entity_type, $entity);
@@ -598,6 +718,18 @@ function metatag_entity_insert($entity, $entity_type) {
$langcode = LANGUAGE_NONE;
}
+ // Work-around for initial entity creation where a language was selection
+ // but where it's different to the form's value.
+ if (!isset($entity->metatags[$langcode]) && isset($entity->metatags[LANGUAGE_NONE])) {
+ $entity->metatags[$langcode] = $entity->metatags[LANGUAGE_NONE];
+ unset($entity->metatags[LANGUAGE_NONE]);
+ }
+
+ // Support for Workbench Moderation v1.
+ if ($entity_type == 'node' && _metatag_isdefaultrevision($entity)) {
+ return;
+ }
+
metatag_metatags_save($entity_type, $entity_id, $revision_id, $entity->metatags, $langcode);
}
}
@@ -611,29 +743,54 @@ function metatag_entity_update($entity, $entity_type) {
}
list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
if (isset($entity->metatags)) {
- // Determine the entity's language.
+ // Determine the entity's new language. This will always be accurate as the
+ // language value will already have been updated by the time this function
+ // executes, and it will be loaded for the correct edit process.
$new_language = metatag_entity_get_language($entity_type, $entity);
- // Determine the language for this entity object.
- if (isset($entity->original)) {
- $old_language = metatag_entity_get_language($entity_type, $entity->original);
+ // If applicable, determine the entity's original language. This cannot be
+ // obtained via the normal API as that data will already have been updated,
+ // instead check to see if the entity has an old-fasioned 'language' value.
+ if (isset($entity->original) && isset($entity->language) && isset($entity->original->language)) {
+ $old_language = $entity->original->language;
- // If the language has changed then remove the old one. When a new
- // translation is being saved using Entity Translation both values will
- // be the same, so this is safe to do.
- if ($old_language != $new_language) {
- db_delete('metatag')
- ->condition('entity_type', $entity_type)
- ->condition('entity_id', $entity_id)
- ->condition('language', $old_language)
- ->execute();
+ // If the language has changed then additional checking needs to be done.
+ // Need to compare against the entity's raw language value as they will
+ // be different when updating a translated entity, versus an untranslated
+ // entity or a source entity for translation, and give a false positive.
+ if ($new_language == $entity->language && $new_language != $old_language) {
+ // If this entity is not translated, or if it is translated but the
+ // translation was previously created, then some language cleanup needs
+ // to be done.
+ if (!isset($entity->translation) || (isset($entity->translation) && !empty($entity->translation['created']))) {
+ // Delete the old language record. This will not affect old revisions.
+ db_delete('metatag')
+ ->condition('entity_type', $entity_type)
+ ->condition('entity_id', $entity_id)
+ ->condition('revision_id', $revision_id)
+ ->condition('language', $old_language)
+ ->execute();
+
+ // Swap out the metatag values for the two languages.
+ if (isset($entity->metatags[$old_language])) {
+ $entity->metatags[$new_language] = $entity->metatags[$old_language];
+ unset($entity->metatags[$old_language]);
+ }
+ }
}
}
+ // Support for Workbench Moderation v1.
+ if ($entity_type == 'node' && _metatag_isdefaultrevision($entity)) {
+ return;
+ }
+
// Save the record.
- metatag_metatags_save($entity_type, $entity_id, $revision_id, $entity->metatags, $new_language);
+ $old_vid = isset($entity->old_vid) ? $entity->old_vid : NULL;
+ metatag_metatags_save($entity_type, $entity_id, $revision_id, $entity->metatags, $new_language, $old_vid);
}
else {
// Still ensure the meta tag output is cached.
@@ -654,21 +811,10 @@ function metatag_entity_delete($entity, $entity_type) {
*/
function metatag_field_attach_delete_revision($entity_type, $entity) {
list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
metatag_metatags_delete($entity_type, $entity_id, $revision_id);
}
-/**
- * Implements hook_taxonomy_term_view_alter().
- */
-function metatag_taxonomy_term_view_alter(&$build, &$entity_type) {
- // This is only needed if hook_entity_view has not been added to core.
- // @see http://drupal.org/node/1067120
- if (isset($build['#term']) && !function_exists('taxonomy_term_view_multiple')) {
- $entity = taxonomy_term_load($build['#term']->tid);
- metatag_entity_view($entity, $entity_type, 'full', NULL, TRUE);
- }
-}
-
/**
* Implements hook_entity_view().
*
@@ -690,92 +836,158 @@ function metatag_entity_view($entity, $entity_type, $view_mode, $langcode, $forc
$i_will_say_this_only_once = TRUE;
}
- // If this entity object isn't allowed meta tags, skip it.
- if (!metatag_entity_has_metatags($entity_type, $entity)) {
- return;
+ // CTools uses 'page_manager' view mode to indicate the full entity display
+ // page rather than 'full', so streamline the internal processes.
+ if ($view_mode == 'page_manager') {
+ $view_mode = 'full';
}
- // Obbtain some details of the entity that are needed elsewhere.
- list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
- $instance = "{$entity_type}:{$bundle}";
-
- // Determine the language this entity actually uses.
- $entity_language = metatag_entity_get_language($entity_type, $entity);
-
- // The requested language is different to the entity's language, look for
- // a language elsewhere.
- if ($entity_language != $langcode) {
- // No language was defined for the entity.
- if ($entity_language == LANGUAGE_NONE) {
- $langcode = LANGUAGE_NONE;
- }
- else {
- $enabled_languages = field_content_languages();
- foreach (field_language($entity_type, $entity) as $field => $lang) {
- // Only accept actual language values that are properly enabled.
- if ($lang != LANGUAGE_NONE && in_array($lang, $enabled_languages)) {
- $langcode = $lang;
- }
- }
- }
+ // Generate metatags output.
+ if ($output = metatag_generate_entity_metatags($entity, $entity_type, $langcode, $view_mode)) {
+ list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+ $instance = "{$entity_type}:{$bundle}";
+ // We need to register the term's metatags, so we can later fetch them.
+ // @see metatag_page_build().
+ metatag_page_set_metatags($instance, $output);
}
+ }
+}
+/**
+ * Generate the metatags for a given entity.
+ *
+ * @param object $entity
+ * The entity object to generate the metatags for.
+ * @param string $entity_type
+ * The entity type of the entity.
+ * @param string $langcode
+ * The language code used for rendering the entity.
+ * @param string $view_mode
+ * The view mode the entity is rendered in.
+ * @param bool $cached
+ * TRUE if metatags can be loaded from and saved to the cache. FALSE if the
+ * cache should be bypassed.
+ *
+ * @return array
+ * A renderable array of metatags for the given entity.
+ */
+function metatag_generate_entity_metatags($entity, $entity_type, $langcode = NULL, $view_mode = 'full', $cached = TRUE) {
+ // If this entity object isn't allowed meta tags, don't continue.
+ if (!metatag_entity_has_metatags($entity_type, $entity)) {
+ return array();
+ }
+
+ // Obtain some details of the entity that are needed elsewhere.
+ list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
+
+ // Check if a specific metatag config exists, otherwise just use the global
+ // one, stripping out the bundle.
+ $instance = "{$entity_type}:{$bundle}";
+ if (!metatag_config_load_with_defaults($instance, FALSE)) {
+ $instance = "{$entity_type}";
+ }
+
+ // Determine the language this entity actually uses.
+ $entity_language = metatag_entity_get_language($entity_type, $entity);
+
+ // If no language was requested, try the language defined for this page
+ // request.
+ if (empty($langcode)) {
+ $langcode = $GLOBALS['language_content']->language;
+ }
+
+ // This entity doesn't have any languages defined, i.e. it uses 'und'. This
+ // can't conflict with loading the wrong language as entities either have no
+ // language or they have specific one(s), they can't have both.
+ if ($entity_language == LANGUAGE_NONE) {
+ $langcode = LANGUAGE_NONE;
+ }
+
+ // If there are no meta tags for the currently identified language, and there
+ // *are* meta tags defined for the entity's default language, use the entity's
+ // default language's values, unless the "Don't load entity's default
+ // language values if no languages match" option is enabled on the advanced
+ // settings page.
+ elseif (empty($entity->metatags[$langcode]) && !empty($entity->metatags[$entity_language]) && !variable_get('metatag_entity_no_lang_default', FALSE)) {
+ $langcode = $entity_language;
+ }
+
+ // Other scenarios.
+ else {
+ // There's no need to do anything else - either there are meta tag values
+ // created for the requested language or there aren't.
+ }
+
+ $cid = FALSE;
+ if ($cached) {
// All applicable pieces for this current page.
$cid_parts = array(
'entity_type' => $entity_type,
'bundle' => $bundle,
'entity_id' => $entity_id,
- 'view_mode' => $view_mode,
+ 'revision_id' => $revision_id,
+ // Cache separately based on the language of the passed-in entity and the
+ // overall active language of the page.
'langcode' => $langcode,
- 'url' => $GLOBALS['base_url'] . base_path() . current_path(),
+ 'language_content' => $GLOBALS['language_content']->language,
+ 'view_mode' => $view_mode,
);
-
- // Allow each page in a sequence to have different values.
- if (isset($_GET['page'])) {
- $cid_parts['page'] = $_GET['page'];
- }
-
- // Allow other modules to alter the page parts using
- // hook_metatag_page_cache_cid_parts_alter().
- drupal_alter('metatag_page_cache_cid_parts', $cid_parts);
-
- $cid = "output:{$entity_type}:{$entity_id}:{$revision_id}:{$langcode}:" . hash('sha256', serialize($cid_parts));
-
- if ($cache = cache_get($cid, 'cache_metatag')) {
- $output = $cache->data;
- }
- else {
- // Separate the meta tags.
- $metatags = isset($entity->metatags) ? $entity->metatags : array();
-
- // Build options for meta tag rendering.
- $options = array(
- 'entity' => $entity,
- 'entity_type' => $entity_type,
- 'view_mode' => $view_mode,
- );
-
- // Ensure we actually pass a language object rather than language code.
- $languages = language_list();
- if (isset($languages[$langcode])) {
- $options['language'] = $languages[$langcode];
- }
-
- // Reload the entity object from cache as it may have been altered.
- $token_type = token_get_entity_mapping('entity', $entity_type);
- $entities = entity_load($entity_type, array($entity_id));
- $options['token data'][$token_type] = $entities[$entity_id];
- $options['entity'] = $entities[$entity_id];
-
- // Render the metatags and save to the cache.
- $output = metatag_metatags_view($instance, $metatags, $options);
- cache_set($cid, $output, 'cache_metatag');
- }
-
- // We need to register the term's metatags, so we can later fetch them.
- // @see metatag_page_build().
- metatag_page_set_metatags($instance, $output);
+ $cid = metatag_cache_default_cid_parts($cid_parts);
}
+
+ if ($cid && $cache = metatag_cache_get($cid)) {
+ $output = $cache->data;
+ }
+ else {
+ // Separate the meta tags.
+ $metatags = isset($entity->metatags) ? $entity->metatags : array();
+
+ // Build options for meta tag rendering.
+ $options = array(
+ 'entity' => $entity,
+ 'entity_type' => $entity_type,
+ 'view_mode' => $view_mode,
+ );
+ // Ensure we actually pass a language object rather than language code.
+ $languages = language_list();
+ if (isset($languages[$langcode])) {
+ $options['language'] = $languages[$langcode];
+ }
+
+ // Reload the entity object from cache as it may have been altered.
+ $token_type = token_get_entity_mapping('entity', $entity_type);
+ $entities = entity_load($entity_type, array($entity_id));
+ $options['token data'][$token_type] = $entities[$entity_id];
+ $options['entity'] = $entities[$entity_id];
+
+ // Render the metatags and save to the cache.
+ $output = metatag_metatags_view($instance, $metatags, $options);
+ if ($cid) {
+ metatag_cache_set($cid, $output);
+ }
+ }
+
+ return $output;
+}
+
+/**
+ * Generate the metatags for a given entity.
+ *
+ * @param object $entity_id
+ * The entity id of the entity to generate the metatags for.
+ * @param string $entity_type
+ * The entity type of the entity to generate the metatags for.
+ * @param string $langcode
+ * The language code used for rendering the entity.
+ *
+ * @return array
+ * A renderable array of metatags for the given entity.
+ */
+function metatags_get_entity_metatags($entity_id, $entity_type, $langcode = NULL) {
+ $entities = entity_load($entity_type, array($entity_id));
+ $entity = reset($entities);
+ return !empty($entity) ? metatag_generate_entity_metatags($entity, $entity_type, $langcode) : array();
}
/**
@@ -830,7 +1042,7 @@ function metatag_metatags_view($instance, array $metatags = array(), array $opti
// Allow the output meta tags to be modified using
// hook_metatag_metatags_view_alter().
- drupal_alter('metatag_metatags_view', $output, $instance);
+ drupal_alter('metatag_metatags_view', $output, $instance, $options);
return $output;
}
@@ -881,6 +1093,12 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
return;
}
+ // Work out the language code to use, default to NONE.
+ $langcode = LANGUAGE_NONE;
+ if (isset($form['#entity_type']) && isset($form['#entity'])) {
+ $langcode = metatag_entity_get_language($form['#entity_type'], $form['#entity']);
+ }
+
// Merge in the default options.
$options += array(
'token types' => array(),
@@ -891,17 +1109,36 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
$form['metatags'] = array(
'#type' => 'fieldset',
'#title' => t('Meta tags'),
- '#multilingual' => TRUE,
'#collapsible' => TRUE,
'#collapsed' => TRUE,
+ '#multilingual' => TRUE,
'#tree' => TRUE,
'#access' => user_access('edit meta tags') || user_access('administer meta tags'),
'#weight' => 40,
+ '#language' => $langcode,
'#attributes' => array(
'class' => array('metatags-form'),
),
- '#metatag_defaults' => $options['defaults'],
);
+ $form['metatags'][$langcode] = array(
+ '#metatag_defaults' => $options['defaults'],
+ '#type' => 'container',
+ '#multilingual' => TRUE,
+ '#tree' => TRUE,
+ );
+ // Show a different intro message for entity pages vs config pages.
+ if (isset($form['#entity'])) {
+ $form['metatags'][$langcode]['intro_text'] = array(
+ '#markup' => '' . t('Configure the meta tags below. Tokens, e.g. "[node:summary]", automatically insert the corresponding information from that field or value, which helps to avoid redundant meta data and possible search engine penalization; see the "Browse available tokens" popup for more details.') . '
',
+ '#weight' => -10,
+ );
+ }
+ else {
+ $form['metatags'][$langcode]['intro_text'] = array(
+ '#markup' => '' . t('Configure the meta tags below. Use tokens (see the "Browse available tokens" popup) to avoid redundant meta data and search engine penalization. For example, a \'keyword\' value of "example" will be shown on all content using this configuration, whereas using the [node:field_keywords] automatically inserts the "keywords" values from the current entity (node, term, etc).') . '
',
+ '#weight' => -10,
+ );
+ }
// Only support vertical tabs if there is a vertical tab element.
foreach (element_children($form) as $key) {
@@ -953,9 +1190,9 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
if (!empty($metatag_info['group'])) {
$group_key = $metatag_info['group'];
- if (isset($info['groups'][$group_key]['label']) && !isset($form['metatags'][$group_key])) {
+ if (isset($info['groups'][$group_key]['label']) && !isset($form['metatags'][$langcode][$group_key])) {
$group = $info['groups'][$group_key] + array('form' => array(), 'description' => NULL);
- $form['metatags'][$group_key] = $group['form'] + array(
+ $form['metatags'][$langcode][$group_key] = $group['form'] + array(
'#type' => 'fieldset',
'#title' => check_plain($group['label']),
'#description' => filter_xss($group['description']),
@@ -963,22 +1200,22 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
'#collapsed' => TRUE,
);
}
- $form['metatags'][$group_key][$metatag] = $metatag_form + array('#parents' => array('metatags', $metatag));
+ $form['metatags'][$langcode][$group_key][$metatag] = $metatag_form + array('#parents' => array('metatags', $langcode, $metatag));
// Hide the fieldset itself if there is not at least one of the meta tag
// fields visible.
if (variable_get('metatag_extended_permissions', FALSE)) {
- $form['metatags'][$group_key]['#access'] = count(element_get_visible_children($form['metatags'][$group_key])) > 0;
+ $form['metatags'][$langcode][$group_key]['#access'] = count(element_get_visible_children($form['metatags'][$langcode][$group_key])) > 0;
}
else {
- $form['metatags'][$group_key]['#access'] = $form['metatags']['#access'];
+ $form['metatags'][$langcode][$group_key]['#access'] = $form['metatags']['#access'];
}
// Structure the access parameter into this array, and make use of it
// later when we move on. Besides, this foreach is getting heavy.
- $group_metatag_access[$group_key] = $form['metatags'][$group_key]['#access'];
+ $group_metatag_access[$group_key] = $form['metatags'][$langcode][$group_key]['#access'];
}
else {
- $form['metatags'][$metatag] = $metatag_form;
+ $form['metatags'][$langcode][$metatag] = $metatag_form;
}
}
@@ -986,7 +1223,7 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
// fields visible; only bother checking this if the user had edit access in
// the first place.
if ($form['metatags']['#access'] && variable_get('metatag_extended_permissions', FALSE)) {
- $form['metatags']['#access'] = count(element_get_visible_children($form['metatags'])) > 0;
+ $form['metatags']['#access'] = count(element_get_visible_children($form['metatags'][$langcode])) > 0;
}
// Check the #access of each group. If it passed, we display options for
@@ -996,20 +1233,20 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
$token_listing_link = theme('token_tree', array('token_types' => $options['token types'], 'dialog' => TRUE));
// Add the token list to the top of the fieldset.
- $form['metatags']['#description'] = $token_listing_link;
+ $form['metatags'][$langcode]['#description'] = $token_listing_link;
// Check if each meta tag group is being displayed.
if (!empty($group_metatag_access)) {
foreach ($group_metatag_access as $group_key => $token_access) {
if ($token_access) {
// Update the description.
- if (isset($form['metatags'][$group_key]['#description'])) {
- $form['metatags'][$group_key]['#description'] .= '
';
+ if (isset($form['metatags'][$langcode][$group_key]['#description'])) {
+ $form['metatags'][$langcode][$group_key]['#description'] .= '
';
}
else {
- $form['metatags'][$group_key]['#description'] = '';
+ $form['metatags'][$langcode][$group_key]['#description'] = '';
}
- $form['metatags'][$group_key]['#description'] .= $token_listing_link;
+ $form['metatags'][$langcode][$group_key]['#description'] .= $token_listing_link;
}
}
}
@@ -1022,11 +1259,32 @@ function metatag_metatags_form(array &$form, $instance, array $metatags = array(
}
/**
- * Form submit handler; unset meta tag values that equal their default values.
+ * Form API submission callback.
+ *
+ * Unset meta tag values that equal their default values, and load any
+ * additional meta tag values for other languages so that they can be properly
+ * saved later on.
+ *
+ * @see metatag_metatags_save()
*/
function metatag_metatags_form_submit($form, &$form_state) {
- if (!empty($form_state['values']['metatags']) && !empty($form['metatags']['#metatag_defaults'])) {
- metatag_filter_values_from_defaults($form_state['values']['metatags'], $form['metatags']['#metatag_defaults']);
+ if (!empty($form_state['values']['metatags'])) {
+ // Unset meta tag values that equal their default values.
+ foreach ($form_state['values']['metatags'] as $langcode => $values) {
+ if (!empty($form['metatags'][$langcode]['#metatag_defaults'])) {
+ metatag_filter_values_from_defaults($form_state['values']['metatags'][$langcode], $form['metatags'][$langcode]['#metatag_defaults']);
+ }
+ }
+
+ // Need to load the entity's values for other languages, otherwise they will
+ // be incorrectly deleted later on.
+ if (isset($form['#entity']) && !empty($form['#entity']->metatags)) {
+ foreach ($form['#entity']->metatags as $langcode => $values) {
+ if (!isset($form_state['values']['metatags'][$langcode])) {
+ $form_state['values']['metatags'][$langcode] = $values;
+ }
+ }
+ }
}
}
@@ -1082,7 +1340,7 @@ function metatag_entity_has_metatags($entity_type, $entity) {
$config_exists[$instance] = metatag_config_is_enabled($instance, TRUE, FALSE);
}
- return isset($config_exists[$instance]);
+ return !empty($config_exists[$instance]);
}
/**
@@ -1110,9 +1368,31 @@ function metatag_entity_supports_metatags($entity_type = NULL, $bundle = NULL) {
}
if (isset($entity_type) && isset($bundle)) {
+ // Allow entities to be disabled by assigning a variable
+ // 'metatag_enable_{$entity_type}__{$bundle}' the value FALSE, e.g.:
+ //
+ // // Disable metatags for carousel nodes.
+ // $conf['metatag_enable_node__carousel'] = FALSE;
+ //
+ // @see Advanced settings page.
+ if (variable_get('metatag_enable_' . $entity_type . '__' . $bundle, 'monkey') == FALSE) {
+ return FALSE;
+ }
+
return isset($entity_types[$entity_type][$bundle]) ? $entity_types[$entity_type][$bundle] : FALSE;
}
elseif (isset($entity_type)) {
+ // Allow entities to be disabled by assigning a variable
+ // 'metatag_enable_{$entity_type}' the value FALSE, e.g.:
+ //
+ // // Disable metatags for file_entity.
+ // $conf['metatag_enable_file'] = FALSE;
+ //
+ // @see Advanced settings page.
+ if (variable_get('metatag_enable_' . $entity_type, 'monkey') == FALSE) {
+ return FALSE;
+ }
+
return isset($entity_types[$entity_type]) ? ($entity_types[$entity_type] !== FALSE) : FALSE;
}
@@ -1129,17 +1409,24 @@ function metatag_entity_info_alter(&$info) {
'path' => 'node/%node',
'metatags' => TRUE,
);
- $defaults['taxonomy_term'] = array(
- 'path' => 'taxonomy/term/%taxonomy_term',
- 'metatags' => TRUE,
- );
- if (module_exists('forum') && ($revision_id = variable_get('forum_nav_vocabulary', 0)) && $vocabulary = taxonomy_vocabulary_load($revision_id)) {
- $defaults['taxonomy_term']['bundles'][$vocabulary->machine_name]['path'] = 'forum/%taxonomy_term';
- }
$defaults['user'] = array(
'path' => 'user/%user',
'metatags' => TRUE,
);
+ $defaults['taxonomy_term'] = array(
+ 'path' => 'taxonomy/term/%taxonomy_term',
+ 'metatags' => TRUE,
+ );
+
+ // Just running taxonomy_vocabulary_load() here would cause problems for
+ // EntityCache in certain circumstances, so instead the query is executed
+ // directly instead.
+ if (module_exists('forum') && ($vocab_id = variable_get('forum_nav_vocabulary', 0))) {
+ $vocab_name = db_query("SELECT machine_name FROM {taxonomy_vocabulary} WHERE vid = :vid", array(':vid' => $vocab_id))->fetchField();
+ if (!empty($vocab_name)) {
+ $defaults['taxonomy_term']['bundles'][$vocab_name]['path'] = 'forum/%taxonomy_term';
+ }
+ }
foreach ($defaults as $key => $entity_defaults) {
if (isset($info[$key])) {
@@ -1202,7 +1489,7 @@ function metatag_page_set_metatags($instance, $metatags) {
}
/**
- * Retrieve the array of met tags to be added with metatag_page_build().
+ * Retrieve the array of meta tags to be added with metatag_page_build().
*/
function metatag_page_get_metatags() {
// @todo Add alter to this result?
@@ -1213,6 +1500,12 @@ function metatag_page_get_metatags() {
* Implements hook_page_build().
*/
function metatag_page_build(&$page) {
+ // By default do not add meta tags to admin pages. To enable meta tags on
+ // admin pages set the 'metatag_tag_admin_pages' variable to TRUE.
+ if (path_is_admin(current_path()) && !variable_get('metatag_tag_admin_pages', FALSE)) {
+ return;
+ }
+
// Ensure these arrays exist, otherwise several use cases will fail.
if (!isset($page['content']) || !is_array($page['content'])) {
$page['content'] = array();
@@ -1221,34 +1514,24 @@ function metatag_page_build(&$page) {
$page['content']['metatags'] = array();
}
- // The front page has special consideration.
+ // The front page has special consideration. Also, check if this is an error
+ // (403/404) page, those also require separate handling.
$instance = 'global:frontpage';
- if (drupal_is_front_page() && metatag_config_is_enabled($instance)) {
- $instance = 'global:frontpage';
+ if ((drupal_is_front_page() && metatag_config_is_enabled($instance))
+ || ($instance = metatag_is_error_page())) {
- // These two parts are sufficient given that the homepage is unique.
+ // Generate the cache ID.
$cid_parts = array(
- 'langcode' => $GLOBALS['language_content']->language,
- 'url' => $GLOBALS['base_url'] . base_path() . '',
+ 'instance' => $instance,
);
+ $cid = metatag_cache_default_cid_parts($cid_parts);
- // Allow each page in a sequence to have different values.
- if (isset($_GET['page'])) {
- $cid_parts['page'] = $_GET['page'];
- }
-
- // Allow other modules to customize the data using
- // hook_metatag_page_cache_cid_parts_alter().
- drupal_alter('metatag_page_cache_cid_parts', $cid_parts);
-
- $cid = "output:{$instance}:" . hash('sha256', serialize($cid_parts));
-
- if ($cache = cache_get($cid, 'cache_metatag')) {
+ if ($cache = metatag_cache_get($cid)) {
$metatags = $cache->data;
}
else {
$metatags = metatag_metatags_view($instance, array());
- cache_set($cid, $metatags, 'cache_metatag');
+ metatag_cache_set($cid, $metatags);
}
$page['content']['metatags'][$instance] = $metatags;
@@ -1260,34 +1543,24 @@ function metatag_page_build(&$page) {
$page['content']['metatags'] += metatag_page_get_metatags();
}
- // If no meta tags were loaded, and this is not an admin path, at least load
- // the global defaults. This may be disabled, see README.txt for details.
- if (empty($page['content']['metatags']) && variable_get('metatag_load_all_pages', TRUE) && !path_is_admin(current_path())) {
+ // If no meta tags were loaded at least load the global defaults. This may be
+ // disabled, see README.txt for details.
+ if (empty($page['content']['metatags']) && variable_get('metatag_load_all_pages', TRUE)) {
$instance = 'global';
- // These two parts are sufficient given that the homepage is unique.
+ // Generate the cache ID.
$cid_parts = array(
- 'langcode' => $GLOBALS['language_content']->language,
- 'url' => $GLOBALS['base_url'] . request_uri(),
+ 'instance' => $instance,
+ 'path' => request_path(),
);
+ $cid = metatag_cache_default_cid_parts($cid_parts);
- // Allow each page in a sequence to have different values.
- if (isset($_GET['page'])) {
- $cid_parts['page'] = $_GET['page'];
- }
-
- // Allow other modules to customize the data using
- // hook_metatag_page_cache_cid_parts_alter().
- drupal_alter('metatag_page_cache_cid_parts', $cid_parts);
-
- $cid = "output:{$instance}:" . hash('sha256', serialize($cid_parts));
-
- if ($cache = cache_get($cid, 'cache_metatag')) {
+ if ($cache = metatag_cache_get($cid)) {
$metatags = $cache->data;
}
else {
$metatags = metatag_metatags_view($instance, array());
- cache_set($cid, $metatags, 'cache_metatag');
+ metatag_cache_set($cid, $metatags);
}
$page['content']['metatags'][$instance] = $metatags;
}
@@ -1306,8 +1579,29 @@ function metatag_page_build(&$page) {
* otherwise.
*/
function _metatag_entity_is_page($entity_type, $entity) {
+ // Exclude comment entities as this conflicts with comment_fragment.module.
+ if ($entity_type == 'comment') {
+ return;
+ }
+
$uri = entity_uri($entity_type, $entity);
- return !empty($uri['path']) && current_path() == $uri['path'];
+ $current_path = current_path();
+
+ // Support for Workbench Moderation v1 - if this is a node, check if the
+ // content type supports moderation.
+ if ($entity_type == 'node' && function_exists('workbench_moderation_node_type_moderated') && workbench_moderation_node_type_moderated($entity->type) === TRUE) {
+ return !empty($uri['path']) && ($current_path == $uri['path'] || $current_path == $uri['path'] . '/draft');
+ }
+
+ // Support for core node revisions.
+ elseif (!empty($uri['path']) && strpos($current_path, $uri['path']) === 0 && strpos($current_path, '/revisions/') && strpos($current_path, '/view')) {
+ return TRUE;
+ }
+
+ // Any other page.
+ else {
+ return !empty($uri['path']) && $current_path == $uri['path'];
+ }
}
/**
@@ -1427,7 +1721,7 @@ function metatag_get_info($type = NULL, $name = NULL) {
// separately.
$cid = 'info:' . $language->language;
- if ($cache = cache_get($cid, 'cache_metatag')) {
+ if ($cache = metatag_cache_get($cid)) {
$info = $cache->data;
}
else {
@@ -1449,7 +1743,7 @@ function metatag_get_info($type = NULL, $name = NULL) {
// hook_metatag_info_alter().
drupal_alter('metatag_info', $info);
- cache_set($cid, $info, 'cache_metatag');
+ metatag_cache_set($cid, $info);
}
}
@@ -1575,7 +1869,7 @@ function metatag_config_instance_info($instance = NULL) {
$cid = 'metatag:config:instance:info:' . $language->language;
if (!isset($info)) {
- if ($cache = cache_get($cid, 'cache_metatag')) {
+ if ($cache = metatag_cache_get($cid)) {
$info = $cache->data;
}
else {
@@ -1587,7 +1881,7 @@ function metatag_config_instance_info($instance = NULL) {
// hook_metatag_config_instance_info_alter().
drupal_alter('metatag_config_instance_info', $info);
- cache_set($cid, $info, 'cache_metatag');
+ metatag_cache_set($cid, $info);
}
}
@@ -1782,19 +2076,22 @@ function metatag_views_post_render(&$view, &$output, &$cache) {
if ($display->display_plugin == 'page' && isset($display->display_options['path'])) {
// Check if this is an entity display page, if so trigger
// hook_entity_view().
- foreach (entity_get_info() as $entity_name => $entity_type) {
+ foreach (entity_get_info() as $entity_type => $entity_info) {
// Entity paths will include an auto-loader that matches the entity's
// name, thus the path will be 'some/path/%entity_name'.
- if (isset($entity_type['path']) && ($display->display_options['path'] . $entity_name) == $entity_type['path']) {
+ if (isset($entity_info['path']) && ($display->display_options['path'] . $entity_type) == $entity_info['path']) {
// Only proceed if this entity type supports meta tags.
- if (metatag_entity_supports_metatags($entity_name)) {
+ if (metatag_entity_supports_metatags($entity_type)) {
// There must be at least one argument and the first argument must be
// numerical.
if (!empty($view->args) && is_numeric($view->args[0])) {
// Only the first argument is used.
- $entities = entity_load($entity_name, array($view->args[0]));
- $entity = array_pop($entities);
- metatag_entity_view($entity, $entity_name, 'full', NULL, TRUE);
+ $entities = entity_load($entity_type, array($view->args[0]));
+ // Only if the entity actually exists.
+ if (!empty($entities)) {
+ $entity = array_pop($entities);
+ metatag_entity_view($entity, $entity_type, 'full', NULL, TRUE);
+ }
}
}
}
@@ -1814,19 +2111,22 @@ function metatag_ctools_render_alter(&$info, $page, $context) {
if ($page && !empty($context['task']['admin path'])) {
// Check if this is an entity display page, if so trigger
// hook_entity_view().
- foreach (entity_get_info() as $entity_name => $entity_type) {
+ foreach (entity_get_info() as $entity_type => $entity_info) {
// Entity paths will include an auto-loader that matches the entity's
// name, thus the path will be 'some/path/%entity_name'.
- if (isset($entity_type['path']) && $context['task']['admin path'] == $entity_type['path']) {
+ if (isset($entity_info['path']) && $context['task']['admin path'] == $entity_info['path']) {
// Only proceed if this entity type supports meta tags.
- if (metatag_entity_supports_metatags($entity_name)) {
+ if (metatag_entity_supports_metatags($entity_type)) {
// There must be at least one argument and the first argument must be
// numerical.
if (!empty($context['args']) && is_numeric($context['args'][0])) {
- // Only the first argument is used.
- $entities = entity_load($entity_name, array($context['args'][0]));
- $entity = array_pop($entities);
- metatag_entity_view($entity, $entity_name, 'full', NULL, TRUE);
+ // We need to pop entity from contexts array.
+ $first_context = array_pop($context['contexts']);
+ // Only if the context actually exists, which would be an entity.
+ if (!empty($first_context->data)) {
+ $langcode = $GLOBALS['language_content']->language;
+ metatag_entity_view($first_context->data, $entity_type, 'full', $langcode, TRUE);
+ }
}
}
}
@@ -1844,6 +2144,7 @@ function metatag_ctools_render_alter(&$info, $page, $context) {
function metatag_entity_translation_delete($entity_type, $entity, $langcode) {
// Get the entity's ID.
list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+ $revision_id = intval($revision_id);
// Delete the translation.
metatag_metatags_delete($entity_type, $entity_id, $revision_id, $langcode);
@@ -1870,3 +2171,130 @@ function metatag_translate($name, $string, $langcode = NULL, $update = FALSE) {
return $string;
}
}
+
+/**
+ * Checks if this entity is the default revision (published).
+ *
+ * @param object $entity
+ * The entity object, e.g., $node.
+ *
+ * @return bool
+ * TRUE if the entity is the default revision, FALSE otherwise.
+ */
+function _metatag_isdefaultrevision($entity) {
+ // D7 "Forward revisioning" is complex and causes a node_save() with the
+ // future node in node table. This fires hook_node_update() twice and cause
+ // abnormal behaviour in metatag.
+ //
+ // The steps taken by Workbench Moderation is to save the forward revision
+ // first and overwrite this with the live version in a shutdown function in
+ // a second step. This will confuse metatag. D7 has no generic property
+ // in the node object, if the node that is updated is the 'published' version
+ // or only a draft of a future version.
+ //
+ // This behaviour will change in D8 where $node->isDefaultRevision has been
+ // introduced. See below links for more details.
+ // - http://drupal.org/node/1879482
+ // - http://drupal.org/node/218755
+ // - http://drupal.org/node/1522154
+ //
+ // Every moderation module saving a forward revision needs to return FALSE.
+ // @todo: Refactor this workaround under D8.
+
+ // Support for Workbench Moderation v1 - if this is a node, check if the
+ // content type supports moderation.
+ if (function_exists('workbench_moderation_node_type_moderated') && workbench_moderation_node_type_moderated($entity->type) === TRUE) {
+ return !empty($entity->workbench_moderation['updating_live_revision']);
+ }
+ return FALSE;
+}
+
+/**
+ * Generate the cache ID to use with metatag_cache_get/metatag_cache_set calls.
+ *
+ * @param array $cid_parts
+ * A list of values to be used.
+ *
+ * @return string
+ * The cache ID string.
+ */
+function metatag_cache_default_cid_parts(array $cid_parts = array()) {
+ // The initial parts, control the order of the parts.
+ $cid_part_defaults = array(
+ 'cache_type' => 'output',
+ 'instance' => '',
+ 'entity_type' => '',
+ 'entity_id' => '',
+ 'bundle' => '',
+ 'langcode' => $GLOBALS['language_content']->language,
+ 'revision_id' => '',
+ 'view_mode' => '',
+ 'status' => 200,
+ 'protocol' => $GLOBALS['is_https'] ? 'https' : 'http',
+ 'hostname' => $_SERVER['HTTP_HOST'],
+ 'base_path' => base_path(),
+ );
+ $cid_parts = array_merge($cid_part_defaults, $cid_parts);
+
+ // Add the HTTP status code.
+ $headers = drupal_get_http_header();
+ if (isset($headers['status'])) {
+ $cid_parts['status'] = intval($headers['status']);
+ }
+
+ // Allow each page in a sequence to have different values.
+ if (isset($_GET['page'])) {
+ $cid_parts['page'] = $_GET['page'];
+ }
+
+ // Allow other modules to customize the data using
+ // hook_metatag_page_cache_cid_parts_alter().
+ drupal_alter('metatag_page_cache_cid_parts', $cid_parts);
+
+ // Remove empty parts.
+ $cid_parts = array_filter($cid_parts);
+
+ // Concatenate the cache ID parts, trim the results to 128 chars.
+ return substr(implode(':', $cid_parts), 0, 128);
+}
+
+/**
+ * Wrapper for cache_set.
+ *
+ * @see cache_set().
+ */
+function metatag_cache_set($cid, $data) {
+ // Cache the data for later.
+ return cache_set($cid, $data, 'cache_metatag');
+}
+
+/**
+ * Wrapper for cache_get.
+ *
+ * @see cache_get().
+ */
+function metatag_cache_get($cid) {
+ // Try to load the object.
+ return cache_get($cid, 'cache_metatag');
+}
+
+/**
+ * Determines if we are in an error page and return the appropriate instance.
+ *
+ * @return string
+ */
+function metatag_is_error_page() {
+ $known_errors = array(
+ 'global:403' => '403 Forbidden',
+ 'global:404' => '404 Not Found',
+ );
+ $headers = drupal_get_http_header();
+ if (isset($headers['status'])) {
+ foreach ($known_errors as $error => $status) {
+ if ($status == $headers['status']) {
+ return $error;
+ }
+ }
+ }
+ return '';
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.test b/sites/all/modules/contrib/seo/metatag/metatag.test
index ad5076ec..bc44e159 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.test
+++ b/sites/all/modules/contrib/seo/metatag/metatag.test
@@ -15,7 +15,7 @@ class MetaTagsUnitTest extends MetaTagsTestHelper {
return array(
'name' => 'Meta tag unit tests',
'description' => 'Test basic meta tag functionality.',
- 'group' => 'Meta tags',
+ 'group' => 'Metatag',
);
}
@@ -94,3 +94,168 @@ class MetaTagsUnitTest extends MetaTagsTestHelper {
}
}
}
+
+
+// TODO: Test each meta tag.
+// TODO: Scenarios.
+//
+// 1. Node
+// * No language assignment.
+// * First save.
+//
+// 2. Node
+// * No language assignment.
+// * Edit existing revision.
+//
+// 3. Node
+// * No language assignment.
+// * Create new revision.
+// * Publish new revision.
+//
+// 4. Node
+// * No language assignment.
+// * Create new revision.
+// * Delete new revision.
+//
+// 5. Node + Translation
+// * No language assignment
+// * Change language assignment.
+// * Edit existing revision.
+//
+// 6. Node + Translation
+// * No language assignment
+// * Change language assignment.
+// * Create new revision.
+// * Publish new revision.
+//
+// 7. Node + Translation
+// * No language assignment
+// * Change language assignment.
+// * Create new revision.
+// * Delete new revision.
+//
+// 8. Node + Translation
+// * Initial language assignment
+//
+// 9. Node + Translation
+// * Initial language assignment
+// * Create new revision.
+// * Publish new revision.
+//
+// 10. Node + Translation
+// * Initial language assignment
+// * Create new revision.
+// * Delete new revision.
+//
+// 11. Node + Translation
+// * Initial language assignment
+// * Change language assignment.
+// * Create new revision.
+// * Publish new revision.
+//
+// 12. Node + Translation
+// * Initial language assignment
+// * Change language assignment.
+// * Create new revision.
+// * Delete new revision.
+//
+// 13. Node + Translation
+// * Initial language assignment
+// * Create translated node.
+//
+// 14. Node + Translation
+// * Initial language assignment
+// * Create new revision.
+// * Publish new revision.
+// * Create translated node.
+//
+// 15. Node + Translation
+// * Initial language assignment
+// * Create new revision.
+// * Publish new revision.
+// * Create translated node.
+// * Delete translated node.
+//
+// 16. Node + Translation
+// * Initial language assignment
+// * Create translated node.
+// * Delete original node.
+//
+// 17. Node + Translation
+// * Initial language assignment
+// * Create new revision.
+// * Publish new revision.
+// * Create translated node.
+// * Delete original node.
+//
+// 18. Node + entity_translation
+// * Initial language assignment
+// * Create translated node.
+//
+// 19. Node + entity_translation
+// * Initial language assignment
+// * Create translated node.
+// * Delete original.
+//
+// 20. Node + entity_translation
+// * Initial language assignment
+// * Create translated node.
+// * Create new revision.
+// * Publish new revision.
+//
+// 21. Node + entity_translation
+// * Initial language assignment
+// * Create translated node.
+// * Create new revision.
+// * Publish new revision.
+// * Delete new revision.
+//
+// 22. Node + entity_translation
+// * Initial language assignment
+// * Create translated node.
+// * Create new revision.
+// * Publish new revision.
+// * Delete original.
+//
+// 23. Node + entity_translation
+// * Initial language assignment
+// * Create translated node.
+// * Create new revision.
+// * Publish new revision.
+// * Delete original.
+//
+// 24. Node + entity_translation
+// * Initial language assignment
+// * Create new revision.
+// * Publish new revision.
+// * Create translated node.
+//
+// 25. Node + entity_translation
+// * Initial language assignment
+// * Create new revision.
+// * Publish new revision.
+// * Create translated node.
+// * Delete new revision.
+//
+//
+// 30. Node + i18n
+//
+//
+// 50. Term
+// * Create term.
+//
+// 51. Term
+// * Create term.
+// * Change values.
+//
+//
+// 60. User
+// * Create user.
+//
+// 61. User
+// * Create user.
+// * Change values.
+//
+//
+// 70. Custom path
+// * Defaults loaded.
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.theme.inc b/sites/all/modules/contrib/seo/metatag/metatag.theme.inc
index 20f4fa2f..7b495ed6 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.theme.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.theme.inc
@@ -56,3 +56,16 @@ function theme_metatag_link_rev($variables) {
unset($element['#value']);
return theme('html_tag', $variables);
}
+
+/**
+ * Theme callback for a proprty meta tag.
+ *
+ * The format is:
+ *
+ */
+function theme_metatag_property($variables) {
+ $element = &$variables['element'];
+ element_set_attributes($element, array('#name' => 'property', '#value' => 'content'));
+ unset($element['#value']);
+ return theme('html_tag', $variables);
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.tokens.inc b/sites/all/modules/contrib/seo/metatag/metatag.tokens.inc
index 634ef061..4fbf0522 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.tokens.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag.tokens.inc
@@ -84,67 +84,6 @@ function metatag_tokens($type, $tokens, array $data = array(), array $options =
return $replacements;
}
-/**
- * Implements hook_tokens_alter().
- *
- * Fix [node:summary] until http://drupal.org/node/1295524 is committed, this
- * code is retrofitted from the patches in that issue.
- */
-function metatag_tokens_alter(array &$replacements, array $context) {
- // Only proceed if this is working on a node.
- if ($context['type'] == 'node' && !empty($context['data']['node'])) {
- // Loop through each of the tokens.
- foreach ($context['tokens'] as $name => $original) {
- // Only deal with the 'node:summary' token, that's the one being fixed.
- if ($name == 'summary') {
- // A shortcut to the node being processed.
- $node = $context['data']['node'];
-
- // Work out the langcode being used.
- if (isset($context['options']['language'])) {
- $langcode = $context['options']['language']->language;
- }
- else {
- $langcode = NULL;
- }
-
- // Decide whether the string needs to be sanitized.
- $sanitize = !empty($context['options']['sanitize']);
-
- if ($items = field_get_items('node', $node, 'body', $langcode)) {
- $instance = field_info_instance('node', 'body', $node->type);
- $field_langcode = field_language('node', $node, 'body', $langcode);
-
- // If the summary is not empty, use it.
- if (!empty($items[0]['summary'])) {
- $output = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], 'summary') : $items[0]['summary'];
- }
-
- // Attempt to provide a suitable version of the 'body' field.
- else {
- $output = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], 'value') : $items[0]['value'];
- // A summary was requested.
- if ($name == 'summary') {
- if (isset($instance['display']['teaser']['settings']['trim_length'])) {
- $trim_length = $instance['display']['teaser']['settings']['trim_length'];
- }
- else {
- // Use default value.
- $trim_length = NULL;
- }
- // Generate an optionally trimmed summary of the body field.
- $output = text_summary($output, $instance['settings']['text_processing'] ? $items[0]['format'] : NULL, $trim_length);
- }
- }
-
- // Override the original value.
- $replacements[$original] = $output;
- }
- }
- }
- }
-}
-
/**
* Generate an array of meta tags for a given entity.
*/
@@ -157,7 +96,13 @@ function metatag_token_generate_array($entity, $entity_type, $bundle) {
$options['token data'][$token_type] = $entity;
$options['entity'] = $entity;
- $metatags = isset($entity->metatags) ? $entity->metatags : array();
+ $metatags = array();
+ if (!empty($entity->metatags)) {
+ $language = metatag_entity_get_language($entity_type, $entity);
+ if (!empty($entity->metatags[$language])) {
+ $metatags = $entity->metatags[$language];
+ }
+ }
$metatags += metatag_config_load_with_defaults($instance);
$result = array();
diff --git a/sites/all/modules/contrib/seo/metatag/metatag.vertical-tabs.js b/sites/all/modules/contrib/seo/metatag/metatag.vertical-tabs.js
index b207b2cf..242cf615 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag.vertical-tabs.js
+++ b/sites/all/modules/contrib/seo/metatag/metatag.vertical-tabs.js
@@ -1,14 +1,24 @@
+/**
+ * @file
+ * Custom JS for controlling the Metatag vertical tab.
+ */
(function ($) {
+ 'use strict';
Drupal.behaviors.metatagFieldsetSummaries = {
attach: function (context) {
$('fieldset.metatags-form', context).drupalSetSummary(function (context) {
var vals = [];
$("input[type='text'], select, textarea", context).each(function() {
- var default_name = $(this).attr('name').replace(/\[value\]/, '[default]');
+ var input_field = $(this).attr('name');
+ // Verify the field exists before proceeding.
+ if (input_field === undefined) {
+ return false;
+ }
+ var default_name = input_field.replace(/\[value\]/, '[default]');
var default_value = $("input[type='hidden'][name='" + default_name + "']", context);
- if (default_value.length && default_value.val() == $(this).val()) {
+ if (default_value.length && default_value.val() === $(this).val()) {
// Meta tag has a default value and form value matches default value.
return true;
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.admin.inc b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.admin.inc
index 4ef711ae..2ca07ce9 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.admin.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.admin.inc
@@ -19,8 +19,8 @@ function metatag_context_context_overview() {
// metatag admin page".
if (isset($context->reactions['metatag_context_reaction']['metatag_admin']) && $context->reactions['metatag_context_reaction']['metatag_admin']) {
$ops = array(
- l('Edit', 'admin/config/search/metatags/context/' . $context->name, array('query' => array('destination' => 'admin/config/search/metatags/context'))),
- l('Delete', 'admin/config/search/metatags/context/' . $context->name . '/delete', array('query' => array('destination' => 'admin/config/search/metatags/context'))),
+ l(t('Edit'), 'admin/config/search/metatags/context/' . $context->name, array('query' => array('destination' => 'admin/config/search/metatags/context'))),
+ l(t('Delete'), 'admin/config/search/metatags/context/' . $context->name . '/delete', array('query' => array('destination' => 'admin/config/search/metatags/context'))),
);
$rows[] = array(
$context->name,
@@ -101,8 +101,15 @@ function metatag_context_config_edit_form($form, &$form_state, $context) {
$instance = "";
$options = array();
+ $metatags = $context->reactions['metatag_context_reaction']['metatags'];
+ if (!isset($metatags[LANGUAGE_NONE])) {
+ $metatags = array(
+ LANGUAGE_NONE => $metatags,
+ );
+ }
+
// Load the METATAG form.
- metatag_metatags_form($form, $instance, $context->reactions['metatag_context_reaction']['metatags'], $options);
+ metatag_metatags_form($form, $instance, $metatags[LANGUAGE_NONE], $options);
$form['paths'] = array(
'#title' => 'Path',
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.context.inc b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.context.inc
index fb36aced..9a0822fb 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.context.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.context.inc
@@ -11,11 +11,21 @@ class metatag_context_reaction extends context_reaction {
// Don't care about the instance name, the data is being managed by
// Context and not Metatag.
$instance = "";
+
// Load the previously saved settings.
$data = $this->fetch_from_context($context);
if (!isset($data['metatags'])) {
$data['metatags'] = array();
}
+
+ // Support the pre-v1.0 data structures that were not nested with the
+ // language code.
+ if (!isset($data['metatags'][LANGUAGE_NONE])) {
+ $data['metatags'] = array(
+ LANGUAGE_NONE => $data['metatags'],
+ );
+ }
+
// No options currently available.
$options = array();
@@ -32,7 +42,7 @@ class metatag_context_reaction extends context_reaction {
);
// Load the basic Metatag form.
- metatag_metatags_form($form, $instance, $data['metatags'], $options);
+ metatag_metatags_form($form, $instance, $data['metatags'][LANGUAGE_NONE], $options);
// Stop the meta tag fields appearing within a fieldset.
$form['metatags']['#type'] = 'container';
@@ -43,30 +53,30 @@ class metatag_context_reaction extends context_reaction {
// Flatten the fieldsets because otherwise the Context module will not save
// them properly.
// TODO: Perhaps it can be done in a better way with #tree and #parents?
- foreach (array('advanced', 'dublin-core', 'open-graph') as $fieldset) {
- if (isset($form['metatags'][$fieldset])) {
- $form['metatags'][$fieldset . '_heading'] = array(
+ foreach (array('advanced', 'dublin-core', 'open-graph', 'twitter-cards') as $fieldset) {
+ if (isset($form['metatags'][LANGUAGE_NONE][$fieldset])) {
+ $form['metatags'][LANGUAGE_NONE][$fieldset . '_heading'] = array(
'#prefix' => '
',
- '#markup' => $form['metatags'][$fieldset]['#title'],
+ '#markup' => $form['metatags'][LANGUAGE_NONE][$fieldset]['#title'],
'#suffix' => '
',
);
- if (isset($form['metatags'][$fieldset]['#description'])) {
- $form['metatags'][$fieldset . '_description'] = array(
+ if (isset($form['metatags'][LANGUAGE_NONE][$fieldset]['#description'])) {
+ $form['metatags'][LANGUAGE_NONE][$fieldset . '_description'] = array(
'#prefix' => '',
- '#markup' => $form['metatags'][$fieldset]['#description'],
+ '#markup' => $form['metatags'][LANGUAGE_NONE][$fieldset]['#description'],
'#suffix' => '
',
);
}
- foreach ($form['metatags'][$fieldset] as $key => $value) {
+ foreach ($form['metatags'][LANGUAGE_NONE][$fieldset] as $key => $value) {
if (substr($key, 0, 1) == '#') {
- unset ($form['metatags'][$fieldset][$key]);
+ unset($form['metatags'][LANGUAGE_NONE][$fieldset][$key]);
continue;
}
- $form['metatags'][$key] = $value;
- unset($form['metatags'][$key]['#parents']);
- unset($form['metatags'][$fieldset][$key]);
+ $form['metatags'][LANGUAGE_NONE][$key] = $value;
+ unset($form['metatags'][LANGUAGE_NONE][$key]['#parents']);
+ unset($form['metatags'][LANGUAGE_NONE][$fieldset][$key]);
}
- unset($form['metatags'][$fieldset]);
+ unset($form['metatags'][LANGUAGE_NONE][$fieldset]);
}
}
@@ -99,6 +109,9 @@ class metatag_context_reaction extends context_reaction {
foreach ($contexts as $context) {
if (!empty($context->reactions['metatag_context_reaction']['metatags'])) {
$metadata_array = $context->reactions['metatag_context_reaction']['metatags'];
+ if (isset($metadata_array[LANGUAGE_NONE])) {
+ $metadata_array = $metadata_array[LANGUAGE_NONE];
+ }
foreach ($metadata_array as $key => $data) {
if (!empty($data['value'])) {
$metatags[$key] = $data;//t(check_plain($data['value']));
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.info b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.info
index df485231..757ded83 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.info
+++ b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.info
@@ -5,10 +5,11 @@ core = 7.x
dependencies[] = context
dependencies[] = metatag
files[] = metatag_context.test
+configure = admin/config/search/metatags/context
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.test b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.test
index a0036b9a..7dfc963f 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.test
+++ b/sites/all/modules/contrib/seo/metatag/metatag_context/metatag_context.test
@@ -14,7 +14,7 @@ class MetatagContextTestCase extends DrupalWebTestCase {
return array(
'name' => 'Meta tag context tests',
'description' => 'Test basic meta tag context functionality.',
- 'group' => 'Meta tags',
+ 'group' => 'Metatag',
);
}
@@ -112,10 +112,10 @@ class MetatagContextTestCase extends DrupalWebTestCase {
function editMetatag($metatag_object) {
$edit_metatag = array(
'paths' => $metatag_object->path,
- 'metatags[title][value]' => $metatag_object->title,
- 'metatags[description][value]' => $metatag_object->description,
- 'metatags[abstract][value]' => $metatag_object->abstract,
- 'metatags[keywords][value]' => $metatag_object->keywords,
+ 'metatags[und][title][value]' => $metatag_object->title,
+ 'metatags[und][description][value]' => $metatag_object->description,
+ 'metatags[und][abstract][value]' => $metatag_object->abstract,
+ 'metatags[und][keywords][value]' => $metatag_object->keywords,
);
$this->drupalPost('admin/config/search/metatags/context/' . $metatag_object->name, $edit_metatag, t('Save'));
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_dc/README.txt b/sites/all/modules/contrib/seo/metatag/metatag_dc/README.txt
index d9592188..fb0b0a6c 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_dc/README.txt
+++ b/sites/all/modules/contrib/seo/metatag/metatag_dc/README.txt
@@ -1,7 +1,7 @@
Metatag: Dublin Core
--------------------
-This module adds the fifteen Dublin Core Metadata Element Set [1] to the
-available meta tags, as defined by the Dublin Core Metadata Institute [2].
+This module adds the fifteen Dublin Core Metadata Element Set [1] (plus one) to
+the available meta tags, as defined by the Dublin Core Metadata Institute [2].
The following tags are provided:
* dcterms.contributor
@@ -12,6 +12,7 @@ The following tags are provided:
* dcterms.format
* dcterms.identifier
* dcterms.language
+* dcterms.modified (additional, not part of the DCES)
* dcterms.publisher
* dcterms.relation
* dcterms.rights
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.info b/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.info
index a04baa01..266a8c47 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.info
+++ b/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.info
@@ -4,9 +4,9 @@ package = SEO
core = 7.x
dependencies[] = metatag
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.metatag.inc b/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.metatag.inc
index 7f8094d1..8299431b 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.metatag.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_dc/metatag_dc.metatag.inc
@@ -5,50 +5,60 @@
*/
/**
- * Implements hook_metatag_config_default_alter().
+ * Implements hook_metatag_bundled_config_alter().
*/
-function metatag_dc_metatag_config_default_alter(array &$configs) {
+function metatag_dc_metatag_bundled_config_alter(array &$configs) {
foreach ($configs as &$config) {
switch ($config->instance) {
case 'global':
$config->config += array(
+ 'dcterms.format' => array('value' => 'text/html'),
+ 'dcterms.identifier' => array('value' => '[current-page:url:absolute]'),
'dcterms.title' => array('value' => '[current-page:title]'),
'dcterms.type' => array('value' => 'Text'),
- 'dcterms.format' => array('value' => 'text/html'),
);
break;
case 'global:frontpage':
$config->config += array(
- 'dcterms.title' => array('value' => '[site:name]'),
+ 'dcterms.description' => array('value' => '[site:slogan]'),
'dcterms.identifier' => array('value' => '[site:url]'),
+ 'dcterms.title' => array('value' => '[site:name]'),
+ );
+ break;
+
+ // On error pages point everything to the homepage.
+ case 'global:403':
+ case 'global:404':
+ $config->config += array(
+ 'dcterms.identifier' => array('value' => '[site:url]'),
+ 'dcterms.title' => array('value' => '[site:name]'),
);
break;
case 'node':
$config->config += array(
- 'dcterms.title' => array('value' => '[node:title]'),
- 'dcterms.date' => array('value' => '[node:created:custom:Y-m-d\TH:iP]'),
- 'dcterms.identifier' => array('value' => '[current-page:url:absolute]'),
- 'dcterms.language' => array('value' => '[node:language]'),
'dcterms.creator' => array('value' => '[node:author]'),
+ 'dcterms.date' => array('value' => '[node:created:custom:Y-m-d\TH:iP]'),
+ 'dcterms.modified' => array('value' => '[node:changed:custom:Y-m-d\TH:iP]'),
+ 'dcterms.description' => array('value' => '[node:summary]'),
+ 'dcterms.language' => array('value' => '[node:language]'),
+ 'dcterms.title' => array('value' => '[node:title]'),
);
break;
case 'taxonomy_term':
$config->config += array(
- 'dcterms.title' => array('value' => '[term:name]'),
- 'dcterms.identifier' => array('value' => '[current-page:url:absolute]'),
'dcterms.description' => array('value' => '[term:description]'),
+ 'dcterms.title' => array('value' => '[term:name]'),
);
break;
case 'user':
$config->config += array(
- 'dcterms.title' => array('value' => '[user:name]'),
- 'dcterms.date' => array('value' => '[user:created:custom:Y-m-d\TH:iP]'),
- 'dcterms.identifier' => array('value' => '[current-page:url:absolute]'),
'dcterms.creator' => array('value' => '[user:name]'),
+ 'dcterms.date' => array('value' => '[user:created:custom:Y-m-d\TH:iP]'),
+ 'dcterms.title' => array('value' => '[user:name]'),
);
break;
}
@@ -67,6 +77,10 @@ function metatag_dc_metatag_info() {
'#weight' => 70,
),
);
+
+ // Dublin Core meta tags stack after the Twitter Cards tags.
+ $weight = 70;
+
$info['tags']['dcterms.title'] = array(
'label' => t('Dublin Core Title'),
'description' => t('The name given to the resource.'),
@@ -76,6 +90,7 @@ function metatag_dc_metatag_info() {
'#type' => 'term',
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.creator'] = array(
'label' => t('Dublin Core Creator'),
@@ -85,6 +100,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.subject'] = array(
'label' => t('Dublin Core Subject'),
@@ -94,6 +110,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.description'] = array(
'label' => t('Dublin Core Description'),
@@ -103,6 +120,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.publisher'] = array(
'label' => t('Dublin Core Publisher'),
@@ -112,6 +130,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.contributor'] = array(
'label' => t('Dublin Core Contributor'),
@@ -121,6 +140,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.date'] = array(
'label' => t('Dublin Core Date'),
@@ -130,6 +150,23 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'date',
+ ),
+ );
+ $info['tags']['dcterms.modified'] = array(
+ 'label' => t('Dublin Core Modified Date'),
+ 'description' => t('Date on which the resource was changed.'),
+ 'class' => 'DrupalTextMetaTag',
+ 'group' => 'dublin-core',
+ 'element' => array(
+ '#theme' => 'metatag_dc',
+ ),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'date',
+ ),
);
$info['tags']['dcterms.type'] = array(
'label' => t('Dublin Core Type'),
@@ -144,6 +181,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.format'] = array(
'label' => t('Dublin Core Format'),
@@ -153,6 +191,10 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'devel_generate' => array(
+ 'maxlength' => 1,
+ ),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.identifier'] = array(
'label' => t('Dublin Core Identifier'),
@@ -162,6 +204,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.source'] = array(
'label' => t('Dublin Core Source'),
@@ -171,6 +214,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.language'] = array(
'label' => t('Dublin Core Language'),
@@ -180,6 +224,10 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'devel_generate' => array(
+ 'maxlength' => 1,
+ ),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.relation'] = array(
'label' => t('Dublin Core Relation'),
@@ -189,6 +237,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.coverage'] = array(
'label' => t('Dublin Core Coverage'),
@@ -198,6 +247,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
$info['tags']['dcterms.rights'] = array(
'label' => t('Dublin Core Rights'),
@@ -207,6 +257,7 @@ function metatag_dc_metatag_info() {
'element' => array(
'#theme' => 'metatag_dc',
),
+ 'weight' => ++$weight,
);
return $info;
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_devel/metatag_devel.info b/sites/all/modules/contrib/seo/metatag/metatag_devel/metatag_devel.info
new file mode 100644
index 00000000..1f212800
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_devel/metatag_devel.info
@@ -0,0 +1,13 @@
+name = Metatag:Devel
+description = Provides development / debugging functionality for the Metatag module. Integrates with Devel Generate.
+package = Development
+core = 7.x
+tags[] = developer
+dependencies[] = metatag
+
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
+core = "7.x"
+project = "metatag"
+datestamp = "1412909330"
+
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_devel/metatag_devel.module b/sites/all/modules/contrib/seo/metatag/metatag_devel/metatag_devel.module
new file mode 100644
index 00000000..4d61fd39
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_devel/metatag_devel.module
@@ -0,0 +1,176 @@
+ 'select',
+ '#title' => t('Skip some meta tag values'),
+ '#options' => array(
+ 0 => t("All meta tags given a value"),
+ 2 => t('Every 2nd value'),
+ 3 => t('Every 3rd value'),
+ 4 => t('Every 4th value'),
+ 5 => t('Every 5th value'),
+ ),
+ '#default' => 0,
+ '#description' => t('Control whether all of the meta tags will be given values, or .'),
+ );
+
+ // Move the submit button to after the rest of the fields.
+ $form['submit']['#weight'] = 100;
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * Integrate with Devel Generate.
+ */
+function metatag_devel_node_insert($node) {
+ // Check to see if the node is generated by Devel Generate.
+ if (isset($node->devel_generate)) {
+ $metatags = array();
+
+ // Max length of words.
+ $title_length = $node->devel_generate['title_length'];
+
+ // Pregenerate the URL.
+ $url = url('', array('absolute' => TRUE));
+
+ // Types of images.
+ $image_types = array('jpg' => 'jpg', 'png' => 'png', 'gif' => 'gif');
+
+ // Optionally skip some meta tags.
+ $skip = $node->devel_generate['metatag_skip'];
+
+ // Generate some meta tags.
+ $count = 0;
+ foreach (metatag_get_info('tags') as $tag => $tag_info) {
+ // Optionally skip records.
+ if (empty($skip) || $count == 0 || (($count % $skip) == 0)) {
+ // Default to ten words per tag.
+ $maxlength = 10;
+
+ // By default, just assume a simple text value.
+ $type = 'text';
+ if (strpos($tag, 'title') !== FALSE) {
+ $maxlength = $title_length;
+ }
+
+ // Allow tags to customize the generation settings.
+ if (!empty($tag_info['devel_generate'])) {
+ if (!empty($tag_info['devel_generate']['type'])) {
+ $type = $tag_info['devel_generate']['type'];
+ }
+ if (!empty($tag_info['devel_generate']['maxlength'])) {
+ $maxlength = $tag_info['devel_generate']['maxlength'];
+ }
+ }
+
+ // Work out how to handle possibly more complicated meta tags.
+ elseif (isset($tag_info['form']) && is_array($tag_info['form'])) {
+ // Textarea fields can be longer than other tags.
+ if (isset($tag_info['form']['#type']) && $tag_info['form']['#type'] == 'textarea') {
+ $maxlength = 20;
+ }
+ // Anything with an '#options' value will have one item picked at
+ // random.
+ elseif (isset($tag_info['form']['#options'])) {
+ $type = 'select';
+ }
+ }
+
+ // Simple values.
+ if ($type == 'text') {
+ $metatags[$tag]['value'] = devel_create_greeking($maxlength, TRUE);
+ }
+
+ // Select lists, pick a value at random.
+ elseif ($type == 'select') {
+ // Nested arrays, aka opgroups - collapse it down to one level.
+ if (is_array($tag_info['form']['#options'])) {
+ $options = array();
+ foreach ($tag_info['form']['#options'] as $option => $subopts) {
+ if (is_array($subopts)) {
+ $options += array_keys($subopts);
+ }
+ else {
+ $options[] = $option;
+ }
+ }
+ }
+ else {
+ $options = $tag_info['form']['#options'];
+ }
+ $metatags[$tag]['value'] = array_rand(drupal_map_assoc($options));
+
+ // Support checkboxes, which require the default value be an array.
+ if ($tag_info['class'] == 'DrupalListMetaTag') {
+ $metatags[$tag]['value'] = array($metatags[$tag]['value']);
+ }
+ }
+
+ // URL values.
+ elseif ($type == 'url') {
+ $metatags[$tag]['value'] = $url . strtolower(str_replace(' ', '/', devel_create_greeking($maxlength, TRUE)));
+ }
+
+ // Image URL values.
+ elseif ($type == 'image') {
+ $filepath = strtolower(str_replace(' ', '/', devel_create_greeking($maxlength, TRUE)));
+ $ext = array_rand($image_types);
+ $metatags[$tag]['value'] = $url . $filepath . '.' . $ext;
+ }
+
+ // Integers, generate an integer between 0 and 999.
+ elseif ($type == 'integer') {
+ $metatags[$tag]['value'] = rand(0, 999);
+ }
+
+ // Floats, generate an integer between 0 and 999.
+ elseif ($type == 'float') {
+ $metatags[$tag]['value'] = rand(0, 999) . '.' . rand(0, 999);
+ }
+
+ // Phone numbers will be given the US format of XXX-XXX-XXXX.
+ elseif ($type == 'phone') {
+ $metatags[$tag]['value'] = rand(100, 999) . '-' . rand(100, 999) . '-' . rand(1000, 9999);
+ }
+
+ // Email addresses.
+ elseif ($type == 'email') {
+ $metatags[$tag]['value'] = strtolower(devel_create_greeking(1, TRUE) . '@' . devel_create_greeking(1, TRUE) . '.com');
+ }
+
+ // Canonical URL values - just fill in the absolute URL for the current
+ // page.
+ elseif ($type == 'canonical') {
+ $metatags[$tag]['value'] = '[current-page:url:absolute]';
+ }
+
+ // Twitter usernames.
+ elseif ($type == 'twitter') {
+ $metatags[$tag]['value'] = '@' . devel_create_greeking(1, TRUE);
+ }
+
+ // Replace spaces in keyword fields with commas.
+ if (strpos($tag, 'keyword') !== FALSE) {
+ $metatags[$tag]['value'] = str_replace(' ', ',', $metatags[$tag]['value']);
+ }
+ }
+
+ // Bump the counter.
+ $count++;
+ }
+
+ // Save the meta tags.
+ metatag_metatags_save('node', $node->nid, $node->vid, $metatags, $node->language);
+ }
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.info b/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.info
new file mode 100644
index 00000000..f0491440
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.info
@@ -0,0 +1,12 @@
+name = Metatag: Facebook
+description = "Provides support for Facebook's custom meta tags."
+package = SEO
+core = 7.x
+dependencies[] = metatag
+
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
+core = "7.x"
+project = "metatag"
+datestamp = "1412909330"
+
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.install b/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.install
new file mode 100644
index 00000000..61561de7
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.install
@@ -0,0 +1,5 @@
+ t('Facebook'),
+ 'description' => t("Meta tags used to integrate with Facebook's APIs. Most sites do not need to use these, they are primarily of benefit for sites using either the Facebook widgets, the Facebook Connect single-signon system, or are using Facebook's APIs in a custom way. Sites that do need these meta tags usually will only need to set them globally."),
+ 'form' => array(
+ '#weight' => 55,
+ ),
+ );
+
+ // Facebook meta tags stack after the simple tags.
+ $weight = 20;
+
+ // Default values for each meta tag.
+ $tag_info_defaults = array(
+ 'description' => '',
+ 'class' => 'DrupalTextMetaTag',
+ 'group' => 'facebook',
+ 'element' => array(
+ '#theme' => 'metatag_property',
+ ),
+ );
+
+ $info['tags']['fb:admins'] = array(
+ 'label' => t('Admins'),
+ 'description' => t('A comma-separated list of Facebook user IDs of people who are considered administrators or moderators of this page.'),
+ 'weight' => ++$weight,
+ ) + $tag_info_defaults;
+
+ $info['tags']['fb:app_id'] = array(
+ 'label' => t('Application ID'),
+ 'description' => t('A comma-separated list of Facebook Platform Application IDs applicable for this site.'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
+ ),
+ ) + $tag_info_defaults;
+
+ // If the FB_Social module is installed already, disable the app_id field.
+ if (module_exists('fb_social')) {
+ $info['tags']['fb:app_id']['form']['#disabled'] = TRUE;
+ $info['tags']['fb:app_id']['form']['#description'] = t('The FB_Social module will automatically output this meta tag, go to the FB_Social settings page to customize it.', array('!fb_social' => url('admin/config/user-interface/fb_social')));
+ }
+
+ return $info;
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.module b/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.module
new file mode 100644
index 00000000..d4942cb6
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_facebook/metatag_facebook.module
@@ -0,0 +1,26 @@
+ 1);
+ }
+}
+
+/**
+ * Implements hook_preprocess_html().
+ */
+function metatag_facebook_preprocess_html(&$variables) {
+ $variables['rdf_namespaces'] .= "\n xmlns:fb=\"http://ogp.me/ns/fb#\"";
+}
+
+/*
+fb:admins
+fb:app_id
+*/
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_google_plus/README.txt b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/README.txt
new file mode 100644
index 00000000..5a9951c4
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/README.txt
@@ -0,0 +1,60 @@
+Metatag: Google+
+-----------------
+This module adds support for meta tag configuration for Google+ Snippet [1].
+
+The following Google+ tags are provided:
+
+* itemprop:name
+* itemprop:description
+* itemprop:image
+
+Also itemtype is provided to add schema in the HTML markup as follows:
+
+
+
+
+Usage
+--------------------------------------------------------------------------------
+Page type (itemtype) provides default type options from the Google+ Snippet page
+[1]; to add other types either install select_or_other module [2] or use the
+Metatag hooks (see metatag.api.php).
+
+
+Known Issues
+--------------------------------------------------------------------------------
+- When using Zen or its derived theme, the RDF Namespaces will be serialized
+ into an RDFa 1.1 prefix attribute, which means itemtype will be included in
+ prefix="...". To avoid this problem, this module will not add a itemtype
+ directly to $variable['rdf_namespaces'], instead, it will be necessary to add
+ code manually in the template.php or the custom theme.
+
+ Example code to use and adapt as needed:
+
+/**
+ * Implements template_preprocess_html().
+ *
+ * Add itemtype code for Google+ in the 'html_attributes_array' which is only
+ * available in Zen theme. Note Zen will convert rdf_namespaces to RDFa 1.1 with
+ * prefix, so putting itemtype there will cause it to be added to the
+ * prefix="itemtype=..." attribute.
+ *
+ * @see zen_preprocess_html()
+ */
+function MYTHEME_preprocess_html(&$variables, $hook) {
+ if (module_exists('metatag_google_plus') && isset($variables['itemtype'])) {
+ $variables['html_attributes_array']['itemscope itemtype'] = "http://schema.org/{$variables['itemtype']}";
+ }
+}
+
+
+Credits / Contact
+--------------------------------------------------------------------------------
+Originally developed by Eric Chen [3] and sponsored by Monkii [4].
+
+
+References
+--------------------------------------------------------------------------------
+1: https://developers.google.com/+/web/snippet/
+2. https://drupal.org/project/select_or_other
+3: https://drupal.org/user/265729
+4: http://monkii.com
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.inc b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.inc
new file mode 100644
index 00000000..c5d81de9
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.inc
@@ -0,0 +1,25 @@
+getValue($options);
+ $element['#attached']['metatag_set_preprocess_variable'][] = array('html', 'itemtype', $value);
+ return $element;
+ }
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.info b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.info
new file mode 100644
index 00000000..5092fa6f
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.info
@@ -0,0 +1,15 @@
+name = Metatag: Google+
+description = "Provides support for Google+ 'itemscope' meta tags."
+package = SEO
+core = 7.x
+
+dependencies[] = metatag
+
+files[] = metatag_google_plus.inc
+
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
+core = "7.x"
+project = "metatag"
+datestamp = "1412909330"
+
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.metatag.inc b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.metatag.inc
new file mode 100644
index 00000000..3ed57a34
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.metatag.inc
@@ -0,0 +1,135 @@
+instance) {
+ case 'global':
+ $config->config += array(
+ 'itemprop:name' => array('value' => '[current-page:title]'),
+ );
+ break;
+
+ case 'global:frontpage':
+ $config->config += array(
+ 'itemprop:name' => array('value' => '[current-page:title]'),
+ );
+ break;
+
+ // On error pages point everything to the homepage.
+ case 'global:403':
+ case 'global:404':
+ $config->config += array(
+ 'itemprop:name' => array('value' => '[current-page:title]'),
+ );
+ break;
+
+ case 'node':
+ $config->config += array(
+ 'itemprop:description' => array('value' => '[node:summary]'),
+ 'itemprop:name' => array('value' => '[current-page:title]'),
+ );
+ break;
+
+ case 'taxonomy_term':
+ $config->config += array(
+ 'itemprop:description' => array('value' => '[term:description]'),
+ 'itemprop:name' => array('value' => '[term:name]'),
+ );
+ break;
+
+ case 'user':
+ $config->config += array(
+ 'itemprop:name' => array('value' => '[user:name]'),
+ );
+ if (variable_get('user_pictures')) {
+ $config->config += array(
+ 'itemprop:image' => array('value' => '[user:picture:url]'),
+ );
+ }
+ break;
+ }
+ }
+}
+
+/**
+ * Implements hook_metatag_info().
+ */
+function metatag_google_plus_metatag_info() {
+ $info['groups']['google-plus'] = array(
+ 'label' => t('Google+'),
+ 'description' => t('A set of meta tags specially for controlling the summaries displayed when content is shared on Google+.', array('!url' => 'https://plus.google.com/')),
+ 'form' => array(
+ '#weight' => 70,
+ ),
+ );
+
+ // Google+ meta tags stack after the Twitter Cards tags.
+ $weight = 60;
+
+ // Defaults used for all cards.
+ $defaults = array(
+ 'class' => 'DrupalTextMetaTag',
+ 'group' => 'google-plus',
+ 'element' => array(
+ '#theme' => 'metatag_google_plus',
+ ),
+ );
+
+ $info['tags']['itemtype'] = array(
+ 'label' => t('Page type'),
+ 'description' => t('Schema type. More schema info. If your page type does not exist in options above, please install select_or_other module to enter page type manually.', array('!url' => 'http://schema.org/docs/schemas.html', '!url2' => 'https://drupal.org/project/select_or_other')),
+ 'class' => 'DrupalSchemaMetaTag',
+ 'weight' => ++$weight,
+ 'form' => array(
+ '#type' => 'select',
+ '#options' => array(
+ 'Article' => t('Article'),
+ 'Blog' => t('Blog'),
+ 'Book' => t('Book'),
+ 'Event' => t('Event'),
+ 'LocalBusiness' => t('Local Business'),
+ 'Organization' => t('Organization'),
+ 'Person' => t('Person'),
+ 'Product' => t('Product'),
+ 'Review' => t('Review'),
+ ),
+ '#empty_option' => t('- None -'),
+ ),
+ ) + $defaults;
+
+ if (module_exists('select_or_other')) {
+ $info['tags']['itemtype']['form']['#type'] = 'select_or_other';
+ $info['tags']['itemtype']['form']['#other'] = 'Other (please type a value)';
+ $info['tags']['itemtype']['form']['#multiple'] = FALSE;
+ $info['tags']['itemtype']['form']['#other_unknown_defaults'] = 'other';
+ $info['tags']['itemtype']['form']['#select_type'] = 'select';
+ }
+
+ $info['tags']['itemprop:name'] = array(
+ 'label' => t('Title'),
+ 'description' => t('A Google+ title for the page being shared. Keep keywords towards the front.'),
+ 'weight' => ++$weight,
+ ) + $defaults;
+ $info['tags']['itemprop:description'] = array(
+ 'label' => t('Description'),
+ 'description' => t('Longer form description, you’ve 200 words here that can specifically reference your presence on Google+'),
+ 'weight' => ++$weight,
+ ) + $defaults;
+ $info['tags']['itemprop:image'] = array(
+ 'label' => t('Image URL'),
+ 'description' => t('The URL to a unique image representing the content of the page. Do not use a generic image such as your website logo, author photo, or other image that spans multiple pages. '),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
+ ) + $defaults;
+
+ return $info;
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.module b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.module
new file mode 100644
index 00000000..8b257580
--- /dev/null
+++ b/sites/all/modules/contrib/seo/metatag/metatag_google_plus/metatag_google_plus.module
@@ -0,0 +1,60 @@
+ 1);
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function metatag_google_plus_theme() {
+ $info['metatag_google_plus'] = array(
+ 'render element' => 'element',
+ );
+
+ return $info;
+}
+
+/**
+ * Theme callback for an Google+ meta tag.
+ *
+ * The format is:
+ *
+ */
+function theme_metatag_google_plus($variables) {
+ $element = &$variables['element'];
+
+ // The format is e.g. 'itemprop:name'. Remove 'itemprop:' and store the rest
+ // in '#itemprop'.
+ $element['#itemprop'] = substr($element['#name'], 9);
+ element_set_attributes($element, array('#itemprop' => 'itemprop', '#value' => 'content'));
+ unset($element['#value']);
+
+ return theme('html_tag', $variables);
+}
+
+/**
+ * Implements hook_preprocess_html().
+ *
+ * Add itemtype when available.
+ *
+ * We will not add itemtype in the rdf_namespaces when using Zen and its derived
+ * themes as Zen will serialize RDF Namespaces into an RDFa 1.1 prefix
+ * attribute, which means itemtype will be included in prefix="...".
+ *
+ * @see zen_preprocess_html()
+ */
+function metatag_google_plus_preprocess_html(&$variables) {
+ if (isset($variables['itemtype']) && !function_exists('zen_preprocess_html')) {
+ $variables['rdf_namespaces'] .= "\n itemscope itemtype= \"http://schema.org/{$variables['itemtype']}\"";
+ }
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.info b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.info
index d5e93327..e431b728 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.info
+++ b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.info
@@ -1,12 +1,12 @@
-name = Metatag: Open Graph
-description = Provides support for open graph meta tags.
+name = Metatag:OpenGraph
+description = Provides support for Open Graph Protocol meta tags.
package = SEO
core = 7.x
dependencies[] = metatag
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.install b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.install
index 1d1f8e0e..1d3813e7 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.install
+++ b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.install
@@ -35,3 +35,22 @@ function metatag_opengraph_install() {
drupal_set_message(t('The core RDF module is known to cause validation problems for Open Graph meta tags. Unless it is actually needed for the site, it may be worthwhile to disable the RDF module to avoid any possible problems for the Open Graph integration.'));
}
}
+
+/**
+ * Implementations of hook_update_N().
+ */
+
+/**
+ * Enable the new Metatag:Facebook submodule.
+ */
+function metatag_opengraph_update_7100() {
+ module_enable(array('metatag_facebook'));
+ drupal_set_message(t('Enabled the new Metatag:Facebook submodule. If the Facebook meta tags are not being used then it is safe to disable.'));
+}
+
+/**
+ * Leave a warning about the two og:type value changes.
+ */
+function metatag_opengraph_update_7101() {
+ drupal_set_message(t('The "Movie" and "TV Show" values for the "Content type" open graph meta tag changed, if this site used those values they will need to be manually updated.'));
+}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.metatag.inc b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.metatag.inc
index 5f2ec2af..bd092ae6 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.metatag.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.metatag.inc
@@ -5,49 +5,67 @@
*/
/**
- * Implements hook_metatag_config_default_alter().
+ * Implements hook_metatag_bundled_config_alter().
*/
-function metatag_opengraph_metatag_config_default_alter(array &$configs) {
+function metatag_opengraph_metatag_bundled_config_alter(array &$configs) {
foreach ($configs as &$config) {
switch ($config->instance) {
case 'global':
$config->config += array(
- 'og:type' => array('value' => 'article'),
- 'og:title' => array('value' => '[current-page:title]'),
'og:site_name' => array('value' => '[site:name]'),
+ 'og:title' => array('value' => '[current-page:title]'),
+ 'og:type' => array('value' => 'article'),
'og:url' => array('value' => '[current-page:url:absolute]'),
);
break;
+
case 'global:frontpage':
$config->config += array(
- 'og:type' => array('value' => 'website'),
+ 'og:description' => array('value' => '[site:slogan]'),
'og:title' => array('value' => '[site:name]'),
+ 'og:type' => array('value' => 'website'),
'og:url' => array('value' => '[site:url]'),
);
break;
+
+ // On error pages point everything to the homepage.
+ case 'global:403':
+ case 'global:404':
+ $config->config += array(
+ 'og:title' => array('value' => '[site:name]'),
+ 'og:type' => array('value' => 'website'),
+ 'og:url' => array('value' => '[site:url]'),
+ );
+ break;
+
case 'node':
$config->config += array(
- 'og:title' => array('value' => '[node:title]'),
+ 'article:modified_time' => array('value' => '[node:changed:custom:c]'),
+ 'article:published_time' => array('value' => '[node:created:custom:c]'),
'og:description' => array('value' => '[node:summary]'),
+ 'og:title' => array('value' => '[node:title]'),
+ 'og:updated_time' => array('value' => '[node:changed:custom:c]'),
);
break;
+
case 'taxonomy_term':
$config->config += array(
- 'og:title' => array('value' => '[term:name]'),
'og:description' => array('value' => '[term:description]'),
+ 'og:title' => array('value' => '[term:name]'),
);
break;
+
case 'user':
$config->config += array(
- 'og:type' => array('value' => 'profile'),
'og:title' => array('value' => '[user:name]'),
+ 'og:type' => array('value' => 'profile'),
+ 'profile:username' => array('value' => '[user:name]'),
);
if (variable_get('user_pictures')) {
$config->config += array(
'og:image' => array('value' => '[user:picture:url]'),
);
}
-
break;
}
}
@@ -59,79 +77,54 @@ function metatag_opengraph_metatag_config_default_alter(array &$configs) {
function metatag_opengraph_metatag_info() {
$info['groups']['open-graph'] = array(
'label' => t('Open Graph'),
+ 'description' => t("The Open Graph meta tags are used control how Facebook, LinkedIn an other social networking sites interpret the site's content.", array('@ogp' => 'http://ogp.me/')),
'form' => array(
'#weight' => 50,
),
);
- $info['tags']['fb:admins'] = array(
- 'label' => t('Facebook Admins'),
- 'description' => t('A comma-separated list of Facebook user IDs of people who are considered administrators or moderators of this page. Most sites will only need to assign this on the global settings page.'),
+ // Default values for each meta tag.
+ $og_defaults = array(
+ 'description' => '',
'class' => 'DrupalTextMetaTag',
'group' => 'open-graph',
'element' => array(
- '#theme' => 'metatag_opengraph',
+ '#theme' => 'metatag_property',
),
);
- $info['tags']['fb:app_id'] = array(
- 'label' => t('Facebook Application ID'),
- 'description' => t('A comma-separated list of Facebook Platform Application IDs applicable for this site. Most sites will only need to assign this on the global settings page.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- if (module_exists('fb_social')) {
- $info['tags']['fb:app_id']['form']['#disabled'] = TRUE;
- $info['tags']['fb:app_id']['form']['#description'] = t('The FB_Social module will automatically output this meta tag, go to the FB_Social settings page to customize it.', array('!fb_social' => url('admin/config/user-interface/fb_social')));
- }
+
+ // Open Graph meta tags stack after the Facebook tags.
+ $weight = 25;
$info['tags']['og:site_name'] = array(
- 'label' => t('Open Graph site name'),
- 'description' => t('A human-readable name for your site, e.g., IMDb.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
+ 'label' => t('Site name'),
+ 'description' => t('A human-readable name for the site, e.g., IMDb.'),
'context' => array('global'),
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
-
- $info['tags']['og:title'] = array(
- 'label' => t('Open Graph title'),
- 'description' => t('The title of your object as it should appear within the graph, e.g., The Rock.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
-
- $info['tags']['og:description'] = array(
- 'label' => t('Open Graph description'),
- 'description' => t('A one to two sentence description of your page.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
+ 'weight' => ++$weight,
+ ) + $og_defaults;
$info['tags']['og:type'] = array(
- 'label' => t('Open Graph type'),
- 'description' => t('The type of your object, e.g., movie.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
+ 'label' => t('Content type'),
+ 'description' => t('The type of the content, e.g., movie.'),
+ 'weight' => ++$weight,
'form' => array(
'#type' => 'select',
'#options' => _metatag_opengraph_type_options(),
'#empty_option' => t('- None -'),
),
- );
+ 'devel_generate' => array(
+ 'type' => 'select',
+ ),
+ ) + $og_defaults;
+
+ $info['tags']['og:url'] = array(
+ 'label' => t('Page URL'),
+ 'description' => t('Preferred page location or URL to help eliminate duplicate content for search engines, e.g., http://www.imdb.com/title/tt0117500/.'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'canonical',
+ ),
+ ) + $og_defaults;
if (module_exists('select_or_other')) {
// Enhance the og:type field to support custom types.
@@ -142,172 +135,411 @@ function metatag_opengraph_metatag_info() {
$info['tags']['og:type']['form']['#element_validate'] = array('select_or_other_element_validate');
}
- $info['tags']['og:image'] = array(
- 'label' => t('Open Graph image'),
- 'description' => t('An image URL which should represent your object within the graph. The image must be at least 50px by 50px and have a maximum aspect ratio of 3:1. We support PNG, JPEG and GIF formats.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
+ $info['tags']['og:title'] = array(
+ 'label' => t('Content title'),
+ 'description' => t('The title of the content, e.g., The Rock.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
- $info['tags']['og:url'] = array(
- 'label' => t('Open Graph URL'),
- 'description' => t('The canonical URL of your object that will be used as its permanent ID in the graph, e.g., http://www.imdb.com/title/tt0117500/.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
-
- $info['tags']['og:latitude'] = array(
- 'label' => t('Open Graph Latitude'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:longitude'] = array(
- 'label' => t('Open Graph Longitude'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
-
- $info['tags']['og:street-address'] = array(
- 'label' => t('Open Graph Street Address'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:locality'] = array(
- 'label' => t('Open Graph Locality'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:region'] = array(
- 'label' => t('Open Graph Region'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:postal-code'] = array(
- 'label' => t('Open Graph Postal Code'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:country-name'] = array(
- 'label' => t('Open Graph Country Name'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
-
- $info['tags']['og:email'] = array(
- 'label' => t('Open Graph Email'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:phone_number'] = array(
- 'label' => t('Open Graph Phone Number'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:fax_number'] = array(
- 'label' => t('Open Graph Fax Number'),
- 'description' => '',
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
-
- $info['tags']['og:video'] = array(
- 'label' => t('Open Graph Video (URL)'),
- 'description' => t('A URL to a video file that complements this object.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:video:secure_url'] = array(
- 'label' => t('Open Graph Video Secure'),
- 'description' => t('A URL to a video file that complements this object using the HTTPS protocol.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:video:width'] = array(
- 'label' => t('Open Graph Video Width'),
- 'description' => t('The width of the video.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:video:height'] = array(
- 'label' => t('Open Graph Video Height'),
- 'description' => t('The height of the video.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
- );
- $info['tags']['og:video:type'] = array(
- 'label' => t('Open Graph Video Type'),
- 'description' => t('The type of the video file.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'open-graph',
- 'element' => array(
- '#theme' => 'metatag_opengraph',
- ),
+ $info['tags']['og:determiner'] = array(
+ 'label' => t('Content title determiner'),
+ 'description' => t("The word that appears before the content's title in a sentence. The default ignores this value, the 'Automatic' value should be sufficient if this is actually needed."),
+ 'weight' => ++$weight,
'form' => array(
'#type' => 'select',
'#options' => array(
- 'application/x-shockwave-flash' => 'Flash - playable directly from the feed',
- 'text/html' => 'Separate HTML page',
+ 'auto' => 'Automatic',
+ 'a' => 'A',
+ 'an' => 'An',
+ 'the' => 'The',
+ ),
+ '#empty_option' => t('- Ignore -'),
+ ),
+ 'devel_generate' => array(
+ 'type' => 'select',
+ ),
+ ) + $og_defaults;
+
+ $info['tags']['og:description'] = array(
+ 'label' => t('Content description'),
+ 'description' => t('A one to two sentence description of the content.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+
+ // Basic tags.
+ $info['tags']['og:updated_time'] = array(
+ 'label' => t('Content modification date & time'),
+ 'description' => t("The date this content was last modified, with an optional time value. Needs to be in ISO 8601 format. Can be the same as the 'Article modification date' tag."),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+
+ $info['tags']['og:see_also'] = array(
+ 'label' => t('See also'),
+ 'description' => t('URLs to related content.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+
+ $info['tags']['og:image'] = array(
+ 'label' => t('Image URL'),
+ 'description' => t('The URL of an image which should represent the content. For best results use an image that is at least 1200 x 630 pixels in size, but at least 600 x 316 pixels is a recommended minimum. Supports PNG, JPEG and GIF formats.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:image:secure_url'] = array(
+ 'label' => t('Secure image URL'),
+ 'description' => t('The secure URL (HTTPS) of an image which should represent the content. The image must be at least 50px by 50px and have a maximum aspect ratio of 3:1. Supports PNG, JPEG and GIF formats. All "http://" URLs will automatically be converted to "https://".'),
+ 'multiple' => TRUE,
+ 'secure' => TRUE,
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:image:type'] = array(
+ 'label' => t('Image type'),
+ 'description' => t('The type of image referenced above. Should be either "image/gif" for a GIF image, "image/jpeg" for a JPG/JPEG image, or "image/png" for a PNG image.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+ $info['tags']['og:image:width'] = array(
+ 'label' => t('Image width'),
+ 'description' => t('The width of the above image(s). Note: if both the unsecured and secured images are provided, they should both be the same size.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:image:height'] = array(
+ 'label' => t('Image height'),
+ 'description' => t('The height of the above image(s). Note: if both the unsecured and secured images are provided, they should both be the same size.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
+ ) + $og_defaults;
+
+ $info['tags']['og:latitude'] = array(
+ 'label' => t('Latitude'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'float',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:longitude'] = array(
+ 'label' => t('Longitude'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'float',
+ ),
+ ) + $og_defaults;
+
+ $info['tags']['og:street-address'] = array(
+ 'label' => t('Street address'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+ $info['tags']['og:locality'] = array(
+ 'label' => t('Locality'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+ $info['tags']['og:region'] = array(
+ 'label' => t('Region'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+ $info['tags']['og:postal-code'] = array(
+ 'label' => t('Postal/ZIP code'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+ $info['tags']['og:country-name'] = array(
+ 'label' => t('Country name'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+
+ $info['tags']['og:email'] = array(
+ 'label' => t('Email'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'email',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:phone_number'] = array(
+ 'label' => t('Phone number'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'phone',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:fax_number'] = array(
+ 'label' => t('Fax number'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'phone',
+ ),
+ ) + $og_defaults;
+
+ $info['tags']['og:locale'] = array(
+ 'label' => t('Locale'),
+ 'description' => 'The locale these tags are marked up in, must be in the format language_TERRITORY. Default is en_US.',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'maxlength' => 1,
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:locale:alternate'] = array(
+ 'label' => t('Alternative locales'),
+ 'description' => 'Other locales this content is available in, must be in the format language_TERRITORY, e.g. "fr_FR".',
+ 'weight' => ++$weight,
+ 'multiple' => TRUE,
+ 'devel_generate' => array(
+ 'maxlength' => 1,
+ ),
+ ) + $og_defaults;
+
+ // For the "article" og:type.
+ $article_defaults = array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'og:type',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'article',
),
- '#empty_option' => t('- None -'),
),
);
+ $info['tags']['article:author'] = array(
+ 'label' => t('Article author'),
+ 'description' => t("Links an article to an author's Facebook profile, should be either URLs to the author's profile page or their Facebook profile IDs."),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+ $info['tags']['article:publisher'] = array(
+ 'label' => t('Article publisher'),
+ 'description' => t("Links an article to a publisher's Facebook page."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+ $info['tags']['article:section'] = array(
+ 'label' => t('Article section'),
+ 'description' => t('The primary section of this website the content belongs to.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+ $info['tags']['article:tag'] = array(
+ 'label' => t('Article tag(s)'),
+ 'description' => t('Appropriate keywords for this content.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+ $info['tags']['article:published_time'] = array(
+ 'label' => t('Article publication date & time'),
+ 'description' => t("The date this content was published on, with an optional time value. Needs to be in ISO 8601 format."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+ $info['tags']['article:modified_time'] = array(
+ 'label' => t('Article modification date & time'),
+ 'description' => t("The date this content was last modified, with an optional time value. Needs to be in ISO 8601 format."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+ $info['tags']['article:expiration_time'] = array(
+ 'label' => t('Article expiration date & time'),
+ 'description' => t("The date this content will expire, with an optional time value. Needs to be in ISO 8601 format."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $article_defaults;
+
+ // For the "profile" og:type.
+ $profile_defaults = array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'og:type',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'profile',
+ ),
+ ),
+ );
+ $info['tags']['profile:first_name'] = array(
+ 'label' => t('First name'),
+ 'description' => t("The first name of the person who's Profile page this is."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $profile_defaults;
+ $info['tags']['profile:last_name'] = array(
+ 'label' => t('Last name'),
+ 'description' => t("The person's last name."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $profile_defaults;
+ $info['tags']['profile:username'] = array(
+ 'label' => t('Username'),
+ 'description' => t("A pseudonym / alias of this person."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $profile_defaults;
+ $info['tags']['profile:gender'] = array(
+ 'label' => t('Gender'),
+ 'description' => t("Any of Facebook's gender values should be allowed, the initial two being 'male' and 'female'."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $profile_defaults;
+
+ // Tags related to audio.
+ $info['tags']['og:audio'] = array(
+ 'label' => t('Audio URL'),
+ 'description' => t('The URL to an audio file that complements this object.'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:audio:secure_url'] = array(
+ 'label' => t('Audio secure URL'),
+ 'description' => t('The secure URL to an audio file that complements this object. All "http://" URLs will automatically be converted to "https://".'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:audio:type'] = array(
+ 'label' => t('Audio type'),
+ 'description' => t('The MIME type of the audio file. Examples include "application/mp3" for an MP3 file.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+
+ // For the "book" og:type.
+ $book_defaults = array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'og:type',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'book',
+ ),
+ ),
+ );
+ $info['tags']['book:author'] = array(
+ 'label' => t("Book's author"),
+ 'description' => t("Links to the book's author's Facebook profile, should be either URLs to the author's profile page or their Facebook profile IDs."),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $book_defaults;
+ $info['tags']['book:isbn'] = array(
+ 'label' => t("Book's ISBN"),
+ 'description' => t("The book's International Standard Book Number, which may be in one of several formats."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $book_defaults;
+ $info['tags']['book:release_date'] = array(
+ 'label' => t('Book release date'),
+ 'description' => t("The date this content will expire, with an optional time value. Needs to be in ISO 8601 format."),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $book_defaults;
+ $info['tags']['book:tag'] = array(
+ 'label' => t('Book tags'),
+ 'description' => t('Appropriate keywords for this book.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $book_defaults;
+
+ // For the "video" og:type.
+ $video_defaults = array();
+ // 'dependencies' => array(
+ // array(
+ // 'dependency' => 'og:type',
+ // 'attribute' => 'value',
+ // 'condition' => 'value',
+ // 'value' => 'profile',
+ // ),
+ // ),
+ // );
+ $info['tags']['og:video'] = array(
+ 'label' => t('Video URL'),
+ 'description' => t('The URL to a video file that complements this object.'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:video:secure_url'] = array(
+ 'label' => t('Video secure URL'),
+ 'description' => t('A URL to a video file that complements this object using the HTTPS protocol. All "http://" URLs will automatically be converted to "https://".'),
+ 'secure' => TRUE,
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:video:width'] = array(
+ 'label' => t('Video width'),
+ 'description' => t('The width of the video.'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:video:height'] = array(
+ 'label' => t('Video height'),
+ 'description' => t('The height of the video.'),
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
+ ),
+ ) + $og_defaults;
+ $info['tags']['og:video:type'] = array(
+ 'label' => t('Video type'),
+ 'description' => t('The MIME type of the video file. Examples include "application/x-shockwave-flash" for a Flash video, or "text/html" if this is a standalone web page containing a video.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults;
+ $info['tags']['video:actor'] = array(
+ 'label' => t('Actor(s)'),
+ 'description' => t('Links to the Facebook profiles for actor(s) that appear in the video.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:actor:role'] = array(
+ 'label' => t("Actors' role"),
+ 'description' => t("The roles of the actor(s)."),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:director'] = array(
+ 'label' => t('Director(s)'),
+ 'description' => t('Links to the Facebook profiles for director(s) that worked on the video.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:writer'] = array(
+ 'label' => t('Scriptwriter(s)'),
+ 'description' => t('Links to the Facebook profiles for scriptwriter(s) for the video.'),
+ 'description' => t("Scriptwriters from the video."),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:duration'] = array(
+ 'label' => t('Video duration (seconds)'),
+ 'description' => t('The length of the video in seconds'),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:release_date'] = array(
+ 'label' => t('Release date'),
+ 'description' => t('The date the video was released.'),
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:tag'] = array(
+ 'label' => t('Tag'),
+ 'description' => t('Tag words associated with this video.'),
+ 'multiple' => TRUE,
+ 'weight' => ++$weight,
+ ) + $og_defaults + $video_defaults;
+ $info['tags']['video:series'] = array(
+ 'label' => t('Series'),
+ 'description' => t('The TV show this series belongs to.'),
+ 'weight' => ++$weight,
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'og:type',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'video.episode',
+ ),
+ ),
+ ) + $og_defaults + $video_defaults;
return $info;
}
@@ -319,7 +551,7 @@ function _metatag_opengraph_type_options() {
'sport' => t('Sport'),
),
t('Businesses') => array(
- 'bar' => t('Bar'),
+ 'bar' => t('Bar', array('context' => 'an establishment')),
'company' => t('Company'),
'cafe' => t('Cafe'),
'hotel' => t('Hotel'),
@@ -359,10 +591,12 @@ function _metatag_opengraph_type_options() {
'drink' => t('Drink'),
'food' => t('Food'),
'game' => t('Game'),
- 'movie' => t('Movie'),
'product' => t('Product'),
'song' => t('Song'),
- 'tv_show' => t('TV show'),
+ 'video.movie' => t('Movie'),
+ 'video.tv_show' => t('TV show'),
+ 'video.episode' => t('TV show episode'),
+ 'video.other' => t('Miscellaneous video'),
),
t('Websites') => array(
'blog' => t('Blog'),
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.module b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.module
index f4a191dc..26520a31 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.module
+++ b/sites/all/modules/contrib/seo/metatag/metatag_opengraph/metatag_opengraph.module
@@ -9,6 +9,18 @@ function metatag_opengraph_preprocess_html(&$variables) {
if (!module_exists('rdf')) {
$variables['rdf_namespaces'] .= "\n xmlns:og=\"http://ogp.me/ns#\"";
}
+
+ // @TODO Would it be worth dynamically identifying whether these should be
+ // added, or just output them all?
+
+ // Need an extra namespace for the "article" tags.
+ $variables['rdf_namespaces'] .= "\n xmlns:article=\"http://ogp.me/ns/article#\"";
+ // Need an extra namespace for the "book" tags.
+ $variables['rdf_namespaces'] .= "\n xmlns:book=\"http://ogp.me/ns/book#\"";
+ // Need an extra namespace for the "profile" tags.
+ $variables['rdf_namespaces'] .= "\n xmlns:profile=\"http://ogp.me/ns/profile#\"";
+ // Need an extra namespace for the "video" tags.
+ $variables['rdf_namespaces'] .= "\n xmlns:video=\"http://ogp.me/ns/video#\"";
}
/**
@@ -20,27 +32,6 @@ function metatag_opengraph_ctools_plugin_api($owner, $api) {
}
}
-/**
- * Implements hook_theme().
- */
-function metatag_opengraph_theme() {
- $info['metatag_opengraph'] = array(
- 'render element' => 'element',
- );
-
- return $info;
-}
-
-/**
- * Theme callback for an OpenGraph meta tag.
- */
-function theme_metatag_opengraph($variables) {
- $element = &$variables['element'];
- element_set_attributes($element, array('#name' => 'property', '#value' => 'content'));
- unset($element['#value']);
- return theme('html_tag', $variables);
-}
-
/*
og:title = [node:title] / [user:name]
og:type = article / profile
@@ -74,7 +65,4 @@ og:audio:type
og:upc
og:isbn
-
-fb:admins
-fb:app_id
*/
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.info b/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.info
index 169921a0..d2b6dd7b 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.info
+++ b/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.info
@@ -8,9 +8,9 @@ dependencies[] = metatag
dependencies[] = panels
dependencies[] = token
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.module b/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.module
index 016f715c..89e2844a 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.module
+++ b/sites/all/modules/contrib/seo/metatag/metatag_panels/metatag_panels.module
@@ -56,10 +56,15 @@ function metatag_panels_form($form, $form_state) {
// Don't set any metatag instance name as the configuration data is managed locally within panels.
$instance = '';
$options = array('token types' => $token_types);
- $conf = empty($handler->conf['metatag_panels']) ? array() : $handler->conf['metatag_panels']['metatags'];
+ $metatags = empty($handler->conf['metatag_panels']) ? array() : $handler->conf['metatag_panels']['metatags'];
+
+ // This leaves some possibility for future versions to support translation.
+ if (!isset($metatags[LANGUAGE_NONE])) {
+ $metatags = array(LANGUAGE_NONE => $metatags);
+ }
// Load the metatag form (passed by reference).
- metatag_metatags_form($form, $instance, $conf, $options);
+ metatag_metatags_form($form, $instance, $metatags[LANGUAGE_NONE], $options);
// Modify metatag form defaults.
$form['metatags']['#collapsible'] = FALSE;
@@ -81,7 +86,7 @@ function metatag_panels_form($form, $form_state) {
function metatag_panels_form_submit($form, $form_state) {
$conf = array(
'enabled' => $form_state['values']['metatags_enabled'],
- 'metatags' => $form_state['values']['metatags'],
+ 'metatags' => $form_state['values']['metatags'][LANGUAGE_NONE],
);
$form_state['handler']->conf['metatag_panels'] = $conf;
@@ -99,8 +104,26 @@ function metatag_panels_ctools_render_alter($info, $page, $context) {
return;
}
+ // This leaves some possibility for future versions to support translation.
$metatags = $handler->conf['metatag_panels']['metatags'];
- $metatags += metatag_config_load_with_defaults('');
+ if (!is_array($metatags) || empty($metatags)) {
+ $metatags = array();
+ }
+
+ // If meta tags were found but they're not nested for the language, fix it.
+ // This leaves some possibility for future versions to support translation.
+ if (!empty($metatags) && !isset($metatags[LANGUAGE_NONE])) {
+ $metatags = array(LANGUAGE_NONE => $metatags);
+ }
+
+ // Append global defaults.
+ $all_metatags = array();
+ foreach ($metatags as $langcode => $values) {
+ if (!empty($values)) {
+ $all_metatags = $values + metatag_config_load_with_defaults('');
+ }
+ }
+ $metatags = $all_metatags;
if (empty($metatags)) {
return;
@@ -117,6 +140,13 @@ function metatag_panels_ctools_render_alter($info, $page, $context) {
$tokens[$task_context->keyword] = $task_context->data;
}
+ // Because of page execution order, sometimes the page title does not get set
+ // by Panels in time for metatags to use it, so we'll explicitly set it here
+ // if we need to.
+ if (!empty($info['title'])) {
+ drupal_set_title($info['title'], PASS_THROUGH);
+ }
+
// Build the Metatag.
$options = array(
'instance' => 'panels:' . $handler->name,
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/README.txt b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/README.txt
index d6e6e829..c4d1e40e 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/README.txt
+++ b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/README.txt
@@ -9,7 +9,7 @@ tags are provided:
* twitter:url
* twitter:title
* twitter:description
-* twitter:image
+* twitter:image:src
* twitter:image:width
* twitter:image:height
* twitter:player
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.info b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.info
index bd93d02f..59362f5a 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.info
+++ b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.info
@@ -3,9 +3,9 @@ description = "Provides support for Twitter's Card meta tags."
package = SEO
core = 7.x
dependencies[] = metatag
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.metatag.inc b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.metatag.inc
index 1e5a0561..24cdfb27 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.metatag.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.metatag.inc
@@ -5,29 +5,38 @@
*/
/**
- * Implements hook_metatag_config_default_alter().
+ * Implements hook_metatag_bundled_config_alter().
*/
-function metatag_twitter_cards_metatag_config_default_alter(array &$configs) {
+function metatag_twitter_cards_metatag_bundled_config_alter(array &$configs) {
foreach ($configs as &$config) {
switch ($config->instance) {
case 'global':
$config->config += array(
'twitter:card' => array('value' => 'summary'),
- 'twitter:description' => array('value' => '[site:slogan]'),
- 'twitter:title' => array('value' => '[site:name]'),
+ 'twitter:title' => array('value' => '[current-page:title]'),
'twitter:url' => array('value' => '[current-page:url:absolute]'),
);
break;
case 'global:frontpage':
$config->config += array(
- 'twitter:description' => array('value' => ''),
+ 'twitter:description' => array('value' => '[site:slogan]'),
+ 'twitter:title' => array('value' => '[site:name]'),
+ 'twitter:url' => array('value' => '[site:url]'),
+ );
+ break;
+
+ // On error pages point everything to the homepage.
+ case 'global:403':
+ case 'global:404':
+ $config->config += array(
+ 'twitter:title' => array('value' => '[site:name]'),
+ 'twitter:url' => array('value' => '[site:url]'),
);
break;
case 'node':
$config->config += array(
- 'twitter:card' => array('value' => 'summary'),
'twitter:description' => array('value' => '[node:summary]'),
'twitter:title' => array('value' => '[node:title]'),
);
@@ -35,10 +44,21 @@ function metatag_twitter_cards_metatag_config_default_alter(array &$configs) {
case 'taxonomy_term':
$config->config += array(
- 'twitter:card' => array('value' => 'summary'),
- 'twitter:title'=> array('value' => '[term:name]'),
+ 'twitter:description' => array('value' => '[term:description]'),
+ 'twitter:title' => array('value' => '[term:name]'),
);
break;
+
+ case 'user':
+ $config->config += array(
+ 'twitter:title' => array('value' => '[user:name]'),
+ );
+ if (variable_get('user_pictures')) {
+ $config->config += array(
+ 'twitter:image:src' => array('value' => '[user:picture:url]'),
+ );
+ }
+ break;
}
}
}
@@ -55,160 +75,351 @@ function metatag_twitter_cards_metatag_info() {
),
);
- $info['tags']['twitter:card'] = array(
- 'label' => t('Twitter card type'),
- 'description' => t('Notes: no other fields are required for a Summary card, a Photo card requires the \'image\' field, while a Media player card requires the \'title\', \'description\', \'media player URL\', \'media player width\', \'media player height\' and \'image\' fields.'),
+ // Twitter Cards meta tags stack after the Open Graph tags.
+ $weight = 40;
+
+ // Defaults used for all cards.
+ $defaults = array(
'class' => 'DrupalTextMetaTag',
'group' => 'twitter-cards',
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards',
+ ),
+ );
+
+ $info['tags']['twitter:card'] = array(
+ 'label' => t('Twitter card type'),
+ 'description' => t('Notes: no other fields are required for a Summary card, a Photo card requires the \'image\' field, a Media player card requires the \'title\', \'description\', \'media player URL\', \'media player width\', \'media player height\' and \'image\' fields, a Summary Card with Large Image card requires the \'Summary\' field and the \'image\' field, a Gallery Card requires all the \'Gallery Image\' fields, an App Card requires the \'iPhone app ID\' field, the \'iPad app ID\' field and the \'Google Play app ID\' field, a Product Card requires the \'description\' field, the \'image\' field, the \'Label 1\' field, the \'Data 1\' field, the \'Label 2\' field and the \'Data 2\' field.'),
+ 'weight' => ++$weight,
'form' => array(
'#type' => 'select',
'#options' => array(
'summary' => t('Summary (default)'),
+ 'summary_large_image' => t('Summary with large image'),
'photo' => t('Photo'),
'player' => t('Media player'),
+ 'gallery' => t('Gallery'),
+ 'app' => t('App'),
+ 'product' => t('Product'),
),
'#empty_option' => t('- None -'),
),
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
- ),
- );
+ ) + $defaults;
$info['tags']['twitter:site'] = array(
'label' => t('Site\'s Twitter account'),
'description' => t('The @username for the website, which will be displayed in the Card\'s footer; must include the @ symbol.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
'context' => array('global'),
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'twitter',
),
- );
+ ) + $defaults;
$info['tags']['twitter:site:id'] = array(
'label' => t('Site\'s Twitter account ID'),
'description' => t('The numerical Twitter account ID for the website, which will be displayed in the Card\'s footer.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
'context' => array('global'),
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
),
- );
+ ) + $defaults;
$info['tags']['twitter:creator'] = array(
'label' => t('Creator\'s Twitter account'),
'description' => t('The @username for the content creator / author for this page, including the @ symbol.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'twitter',
),
- );
+ ) + $defaults;
$info['tags']['twitter:creator:id'] = array(
'label' => t('Creator\'s Twitter account ID'),
'description' => t('The numerical Twitter account ID for the content creator / author for this page.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
),
- );
+ ) + $defaults;
$info['tags']['twitter:url'] = array(
'label' => t('Page URL'),
'description' => t('The permalink / canonical URL of the current page.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'canonical',
),
- );
+ ) + $defaults;
$info['tags']['twitter:title'] = array(
'label' => t('Title'),
'description' => t('The page\'s title, which should be concise; it will be truncated at 70 characters by Twitter. This field is required unless this the \'type\' field is set to "photo".'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
- ),
- );
+ 'weight' => ++$weight,
+ ) + $defaults;
$info['tags']['twitter:description'] = array(
'label' => t('Description'),
'description' => t('A description that concisely summarizes the content of the page, as appropriate for presentation within a Tweet. Do not re-use the title text as the description, or use this field to describe the general services provided by the website. The string will be truncated, by Twitter, at the word to 200 characters.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
- ),
- );
- $info['tags']['twitter:image'] = array(
+ 'weight' => ++$weight,
+ ) + $defaults;
+ $info['tags']['twitter:image:src'] = array(
'label' => t('Image URL'),
'description' => t('The URL to a unique image representing the content of the page. Do not use a generic image such as your website logo, author photo, or other image that spans multiple pages. Images larger than 120x120px will be resized and cropped square based on longest dimension. Images smaller than 60x60px will not be shown. If the \'type\' is set to Photo then the image must be at least 280x150px.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'replaces' => array(
+ 'twitter:image',
),
- );
+ 'devel_generate' => array(
+ 'type' => 'image',
+ ),
+ ) + $defaults;
$info['tags']['twitter:image:width'] = array(
'label' => t('Image width'),
'description' => t('The width of the image being linked to, in pixels.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'twitter:image:src',
+ 'attribute' => 'value',
+ 'condition' => 'filled',
+ 'value' => TRUE,
+ ),
),
- );
+ 'devel_generate' => array(
+ 'type' => 'integer',
+ ),
+ ) + $defaults;
$info['tags']['twitter:image:height'] = array(
'label' => t('Image height'),
'description' => t('The height of the image being linked to, in pixels.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'twitter:image:src',
+ 'attribute' => 'value',
+ 'condition' => 'filled',
+ 'value' => TRUE,
+ ),
),
- );
+ 'devel_generate' => array(
+ 'type' => 'integer',
+ ),
+ ) + $defaults;
+
+ // 'gallery' cards.
+ $gallery_defaults = array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'twitter:card',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'gallery',
+ ),
+ ),
+ ) + $defaults;
+ $info['tags']['twitter:image0'] = array(
+ 'label' => t('1st Gallery Image'),
+ 'description' => t('A URL to the image representing the first photo in your gallery.'),
+ 'weight' => ++$weight,
+ ) + $gallery_defaults;
+ $info['tags']['twitter:image1'] = array(
+ 'label' => t('2nd Gallery Image'),
+ 'description' => t('A URL to the image representing the second photo in your gallery.'),
+ 'weight' => ++$weight,
+ ) + $gallery_defaults;
+ $info['tags']['twitter:image2'] = array(
+ 'label' => t('3rd Gallery Image'),
+ 'description' => t('A URL to the image representing the third photo in your gallery.'),
+ 'weight' => ++$weight,
+ ) + $gallery_defaults;
+ $info['tags']['twitter:image3'] = array(
+ 'label' => t('4th Gallery Image'),
+ 'description' => t('A URL to the image representing the fourth photo in your gallery.'),
+ 'weight' => ++$weight,
+ ) + $gallery_defaults;
+
+ // 'player' cards.
+ $player_defaults = array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'twitter:card',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'player',
+ ),
+ ),
+ ) + $defaults;
$info['tags']['twitter:player'] = array(
'label' => t('Media player URL'),
'description' => t('The full URL for loading a media player. Required when using a Media player card.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
),
- );
+ ) + $player_defaults;
$info['tags']['twitter:player:width'] = array(
'label' => t('Media player width'),
'description' => t('The width of the media player iframe, in pixels. Required when using a Media player card.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
),
- );
+ ) + $player_defaults;
$info['tags']['twitter:player:height'] = array(
'label' => t('Media player height'),
'description' => t('The height of the media player iframe, in pixels. Required when using a Media player card.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'integer',
),
- );
+ ) + $player_defaults;
$info['tags']['twitter:player:stream'] = array(
'label' => t('MP4 media stream URL'),
'description' => t('The full URL for an MP4 video (h.264) or audio (AAC) stream, takes precidence over the other media player field.'),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'type' => 'url',
),
- );
+ ) + $player_defaults;
$info['tags']['twitter:player:stream:content_type'] = array(
'label' => t('MP4 media stream MIME type'),
'description' => t('The MIME type for the media contained in the stream URL, as defined by RFC 4337.', array('!url' => 'http://tools.ietf.org/rfc/rfc4337.txt')),
- 'class' => 'DrupalTextMetaTag',
- 'group' => 'twitter-cards',
- 'element' => array(
- '#theme' => 'metatag_twitter_cards',
+ 'weight' => ++$weight,
+ 'devel_generate' => array(
+ 'maxlength' => 1,
+ ),
+ ) + $player_defaults;
+
+ // 'app' cards.
+ $app_defaults = $defaults + array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'twitter:card',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'app',
+ ),
),
);
+ $info['tags']['twitter:app:country'] = array(
+ 'label' => t('App Store Country'),
+ 'description' => t('If your application is not available in the US App Store, you must set this value to the two-letter country code for the App Store that contains your application.'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:name:iphone'] = array(
+ 'label' => t('iPhone app name'),
+ 'description' => t("The name of the iPhone app."),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:id:iphone'] = array(
+ 'label' => t('iPhone app ID'),
+ 'description' => t("String value, should be the numeric representation of your iPhone app's ID in the App Store."),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:url:iphone'] = array(
+ 'label' => t('iPhone app\'s custom URL scheme'),
+ 'description' => t('The iPhone app\'s custom URL scheme (must include "://" after the scheme name).'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:name:ipad'] = array(
+ 'label' => t('iPad app name'),
+ 'description' => t("The name of the iPad app."),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:id:ipad'] = array(
+ 'label' => t('iPad app ID'),
+ 'description' => t("String value, should be the numeric representation of your iPad app's ID in the App Store."),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:url:ipad'] = array(
+ 'label' => t('iPad app\'s custom URL scheme'),
+ 'description' => t('The iPad app\'s custom URL scheme (must include "://" after the scheme name).'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:name:googleplay'] = array(
+ 'label' => t('Google Play app name'),
+ 'description' => t("The name of the app in the Google Play app store."),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:id:googleplay'] = array(
+ 'label' => t('Google Play app ID'),
+ 'description' => t("String value, and should be the numeric representation of your app's ID in Google Play."),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+ $info['tags']['twitter:app:url:googleplay'] = array(
+ 'label' => t('Google Play app\'s custom URL scheme'),
+ 'description' => t('The Google Play app\'s custom URL scheme (must include "://" after the scheme name).'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $app_defaults;
+
+ // 'product' cards.
+ $product_defaults = array(
+ 'dependencies' => array(
+ array(
+ 'dependency' => 'twitter:card',
+ 'attribute' => 'value',
+ 'condition' => 'value',
+ 'value' => 'product',
+ ),
+ ),
+ ) + $defaults;
+ $info['tags']['twitter:label1'] = array(
+ 'label' => t('Label 1'),
+ 'description' => t('This field expects a string, and you can specify values for labels such as price, items in stock, sizes, etc.'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $product_defaults;
+ $info['tags']['twitter:data1'] = array(
+ 'label' => t('Data 1'),
+ 'description' => t('This field expects a string, and allows you to specify the types of data you want to offer (price, country, etc.).'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $product_defaults;
+ $info['tags']['twitter:label2'] = array(
+ 'label' => t('Label 2'),
+ 'description' => t('This field expects a string, and you can specify values for labels such as price, items in stock, sizes, etc.'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $product_defaults;
+ $info['tags']['twitter:data2'] = array(
+ 'label' => t('Data 2'),
+ 'description' => t('This field expects a string, and allows you to specify the types of data you want to offer (price, country, etc.).'),
+ 'weight' => ++$weight,
+ 'element' => array(
+ '#theme' => 'metatag_twitter_cards'
+ ),
+ ) + $product_defaults;
+
return $info;
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.module b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.module
index 2f835c4f..e848fac7 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.module
+++ b/sites/all/modules/contrib/seo/metatag/metatag_twitter_cards/metatag_twitter_cards.module
@@ -29,7 +29,7 @@ function metatag_twitter_cards_theme() {
*/
function theme_metatag_twitter_cards($variables) {
$element = &$variables['element'];
- element_set_attributes($element, array('#name' => 'property', '#value' => 'content'));
+ element_set_attributes($element, array('#name' => 'name', '#value' => 'content'));
unset($element['#value']);
return theme('html_tag', $variables);
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_ui/metatag_ui.info b/sites/all/modules/contrib/seo/metatag/metatag_ui/metatag_ui.info
deleted file mode 100644
index d9c54172..00000000
--- a/sites/all/modules/contrib/seo/metatag/metatag_ui/metatag_ui.info
+++ /dev/null
@@ -1,14 +0,0 @@
-name = Meta tag UI
-description = "DEPRECATED admin interface for the Meta tag API, this functionality has be merged into the main module."
-package = SEO
-core = 7.x
-dependencies[] = metatag
-dependencies[] = ctools
-hidden = TRUE
-
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
-core = "7.x"
-project = "metatag"
-datestamp = "1379942674"
-
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_ui/metatag_ui.module b/sites/all/modules/contrib/seo/metatag/metatag_ui/metatag_ui.module
deleted file mode 100644
index 0942a6b3..00000000
--- a/sites/all/modules/contrib/seo/metatag/metatag_ui/metatag_ui.module
+++ /dev/null
@@ -1,5 +0,0 @@
- 3);
+ return array('api' => 3.0);
}
/**
@@ -25,78 +25,79 @@ function metatag_views_ctools_plugin_api($owner, $api) {
* Implements hook_view_preview_info_alter().
*/
function metatag_views_views_preview_info_alter(&$rows, $view) {
- if (metatag_views_views_display_has_metatags($view->display_handler)) {
- $instance = 'view:' . $view->name;
- $metatags = $view->display_handler->get_option('metatags');
- $metatags = !empty($metatags) ? $metatags : array();
+ $metatags = $view->display_handler->get_option('metatags');
+ if (!is_array($metatags) || empty($metatags)) {
+ return;
+ }
- // Set the page title to be the previewed views title before fetching meta
- // tag values.
- $title = drupal_set_title();
- if ($view_title = $view->get_title()) {
- drupal_set_title($view_title);
- }
+ // If meta tags were found but they're not nested for the language, fix it.
+ // This leaves some possibility for future versions to support translation.
+ if (!empty($metatags) && !isset($metatags[LANGUAGE_NONE])) {
+ $metatags = array(LANGUAGE_NONE => $metatags);
+ }
- $options['token data']['view'] = $view;
- $values = metatag_metatags_values($instance, $metatags, $options);
- foreach ($values as $metatag => $value) {
- $metatag_info = metatag_get_info('tags', $metatag);
- $values[$metatag] = check_plain($metatag_info['label']) . ': ' . check_plain($value);
- }
+ // Set the page title to be the previewed views title before fetching meta
+ // tag values.
+ $title = drupal_set_title();
+ if ($view_title = $view->get_title()) {
+ drupal_set_title($view_title);
+ }
+
+ $instance = 'view:' . $view->name;
+ $options['token data']['view'] = $view;
+ $values = metatag_metatags_values($instance, $metatags, $options);
+ foreach ($values as $metatag => $value) {
+ $metatag_info = metatag_get_info('tags', $metatag);
+ $values[$metatag] = check_plain($metatag_info['label']) . ': ' . check_plain($value);
+ }
+ if (!empty($values)) {
$rows['query'][] = array(
'' . t('Meta tags') . '',
implode('
', $values),
);
-
- // Restore the page title.
- drupal_set_title($title);
}
+
+ // Restore the page title.
+ drupal_set_title($title);
}
/**
* Implements hook_page_alter().
*/
function metatag_views_page_alter(&$page) {
- if ($view = views_get_page_view()) {
+ $view = views_get_page_view();
+
+ // Check if Views metatags are enabled.
+ if (!empty($view) && metatag_config_is_enabled('view')) {
// The following is taken from views_get_page_view().
// If a module is still putting in the display like we used to, catch that.
if (is_subclass_of($view, 'views_plugin_display')) {
$view = $view->view;
}
- // Load the
- if (metatag_views_views_display_has_metatags($view->display_handler)) {
- $saved_metatags = $view->display_handler->get_option('metatags');
- $metatags = array();
- if (!empty($saved_metatags)) {
- $metatags[LANGUAGE_NONE] = $saved_metatags;
- }
-
- // Build options for meta tag rendering.
- $instance = 'view:' . $view->name;
- $options = array();
- $options['token data']['view'] = $view;
-
- // Add the metatags.
- $page['content']['metatags'][$instance] = metatag_metatags_view($instance, $metatags, $options);
+ // Prevent Views settings from overwriting global:frontpage.
+ if (drupal_is_front_page() && metatag_config_is_enabled('global:frontpage')) {
+ return;
}
- }
-}
-/**
- * Determine whether the requested view display has meta tags saved.
- *
- * @param $display views_plugin_display
- * The view display plugin that will be checked.
- *
- * @return
- * Simple boolean to indicate whether there are meta tags saved.
- */
-function metatag_views_views_display_has_metatags(views_plugin_display $display) {
- if (method_exists($display, 'has_metatags')) {
- return $display->has_metatags();
- }
- else {
- return $display->has_path() && $display->uses_breadcrumb();
+ // Load the meta tags for this view.
+ $metatags = $view->display_handler->get_option('metatags');
+ if (!is_array($metatags) || empty($metatags)) {
+ $metatags = array();
+ }
+
+ // If meta tags were found but they're not nested for the language, fix it.
+ // This leaves some possibility for future versions to support translation.
+ if (!empty($metatags) && !isset($metatags[LANGUAGE_NONE])) {
+ $metatags = array(LANGUAGE_NONE => $metatags);
+ }
+
+ // Build options for meta tag rendering.
+ $instance = 'view:' . $view->name;
+ $options = array();
+ $options['token data']['view'] = $view;
+
+ // Add the metatags.
+ $page['content']['metatags'][$instance] = metatag_metatags_view($instance, $metatags, $options);
}
}
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views.views.inc b/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views.views.inc
index 9a0e75e8..54f00c09 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views.views.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views.views.inc
@@ -14,6 +14,7 @@ function metatag_views_views_plugins() {
'help' => t('Provides meta tags for views.'),
'handler' => 'metatag_views_plugin_display_extender_metatags',
'enabled' => TRUE,
+ 'path' => drupal_get_path('module', 'metatag_views'),
);
return $plugins;
diff --git a/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views_plugin_display_extender_metatags.inc b/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views_plugin_display_extender_metatags.inc
index 7c142f1c..4a5d96d2 100644
--- a/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views_plugin_display_extender_metatags.inc
+++ b/sites/all/modules/contrib/seo/metatag/metatag_views/metatag_views_plugin_display_extender_metatags.inc
@@ -6,47 +6,94 @@
class metatag_views_plugin_display_extender_metatags extends views_plugin_display_extender {
- protected function has_metatags() {
- return metatag_views_views_display_has_metatags($this->display);
+ /**
+ * Default values.
+ */
+ function options_definition() {
+ $options = parent::option_definition();
+ $options['metatags'] = array('default' => '');
+ return $options;
}
-
function options_definition_alter(&$options) {
$options['metatags'] = array('default' => array());
}
+ /**
+ * Defines where within the Views admin UI the new settings will be visible.
+ */
function options_summary(&$categories, &$options) {
- if ($this->has_metatags()) {
- $categories['metatags'] = array(
- 'title' => t('Meta tags'),
- 'column' => 'second',
- );
- $metatags = $this->display->get_option('metatags');
- $options['metatags'] = array(
- 'category' => 'metatags',
- 'title' => t('Meta tags'),
- 'value' => !empty($metatags) ? t('Overridden') : t('Using defaults'),
- );
- }
+ $categories['metatags'] = array(
+ 'title' => t('Meta tags'),
+ 'column' => 'second',
+ );
+ $options['metatags'] = array(
+ 'category' => 'metatags',
+ 'title' => t('Meta tags'),
+ 'value' => $this->has_metatags() ? t('Overridden') : t('Using defaults'),
+ );
}
+ /**
+ * Defines the form.
+ */
function options_form(&$form, &$form_state) {
if ($form_state['section'] == 'metatags') {
$form['#title'] .= t('The meta tags for this display');
+ $metatags = $this->get_metatags();
+
+ // Build/inject the Metatag form.
$instance = 'view:' . $form_state['view']->name;
- $metatags = $this->display->get_option('metatags');
- $metatags = !empty($metatags) ? $metatags : array();
$options['token types'] = array('view');
$options['context'] = 'view';
- metatag_metatags_form($form, $instance, $metatags, $options);
+ metatag_metatags_form($form, $instance, $metatags[LANGUAGE_NONE], $options);
+
$form['metatags']['#type'] = 'container';
}
}
+ /**
+ * Saves the form values.
+ */
function options_submit(&$form, &$form_state) {
if ($form_state['section'] == 'metatags') {
$metatags = $form_state['values']['metatags'];
- metatag_filter_values_from_defaults($metatags);
+
+ // Leave some possibility for future versions to support translation.
+ foreach ($metatags as $langcode => $values) {
+ if (!empty($form['metatags'][$langcode]['#metatag_defaults'])) {
+ metatag_filter_values_from_defaults($form_state['values']['metatags'][$langcode], $form['metatags'][$langcode]['#metatag_defaults']);
+ }
+ }
+
$this->display->set_option('metatags', $metatags);
}
}
+
+ /**
+ * Identify whether or not the current display has custom meta tags defined.
+ */
+ protected function has_metatags() {
+ $metatags = $this->get_metatags();
+ return !empty($metatags[LANGUAGE_NONE]);
+ }
+
+ /**
+ * Get the Metatag configuration for this display.
+ *
+ * @return array
+ * The meta tag values, keys by language (default LANGUAGE_NONE).
+ */
+ private function get_metatags() {
+ $metatags = $this->display->get_option('metatags');
+
+ // Leave some possibility for future versions to support translation.
+ if (empty($metatags)) {
+ $metatags = array(LANGUAGE_NONE => array());
+ }
+ if (!isset($metatags[LANGUAGE_NONE])) {
+ $metatags = array(LANGUAGE_NONE => $metatags);
+ }
+
+ return $metatags;
+ }
}
diff --git a/sites/all/modules/contrib/seo/metatag/tests/metatag_test.info b/sites/all/modules/contrib/seo/metatag/tests/metatag_test.info
index 3194d907..cf5584f2 100644
--- a/sites/all/modules/contrib/seo/metatag/tests/metatag_test.info
+++ b/sites/all/modules/contrib/seo/metatag/tests/metatag_test.info
@@ -4,9 +4,9 @@ core = 7.x
dependencies[] = metatag
hidden = TRUE
-; Information added by drupal.org packaging script on 2013-09-23
-version = "7.x-1.0-beta7+54-dev"
+; Information added by Drupal.org packaging script on 2014-10-10
+version = "7.x-1.4"
core = "7.x"
project = "metatag"
-datestamp = "1379942674"
+datestamp = "1412909330"
diff --git a/sites/all/modules/contrib/theming/extlink/extlink.css b/sites/all/modules/contrib/theming/extlink/extlink.css
index 44de35cc..f912d8b8 100644
--- a/sites/all/modules/contrib/theming/extlink/extlink.css
+++ b/sites/all/modules/contrib/theming/extlink/extlink.css
@@ -1,10 +1,16 @@
span.ext {
- background: url(extlink.png) right center no-repeat;
+ background: url(extlink_s.png) 2px center no-repeat;
+ width: 10px;
+ height: 10px;
padding-right: 12px;
+ text-decoration: none;
}
span.mailto {
- background: url(mailto.png) right center no-repeat;
+ background: url(extlink_s.png) -20px center no-repeat;
+ width: 10px;
+ height: 10px;
padding-right: 12px;
+ text-decoration: none;
}
/* Hide the extra spans when printing. */
diff --git a/sites/all/modules/contrib/theming/extlink/extlink.info b/sites/all/modules/contrib/theming/extlink/extlink.info
index 2158dcaf..00d732fe 100644
--- a/sites/all/modules/contrib/theming/extlink/extlink.info
+++ b/sites/all/modules/contrib/theming/extlink/extlink.info
@@ -6,9 +6,9 @@ configure = admin/config/user-interface/extlink
stylesheets[all][] = extlink.css
files[] = extlink.test
-; Information added by drupal.org packaging script on 2013-07-29
-version = "7.x-1.13"
+; Information added by Drupal.org packaging script on 2014-07-25
+version = "7.x-1.18"
core = "7.x"
project = "extlink"
-datestamp = "1375069568"
+datestamp = "1406278728"
diff --git a/sites/all/modules/contrib/theming/extlink/extlink.install b/sites/all/modules/contrib/theming/extlink/extlink.install
index e04b0b5e..eb7662be 100644
--- a/sites/all/modules/contrib/theming/extlink/extlink.install
+++ b/sites/all/modules/contrib/theming/extlink/extlink.install
@@ -1,7 +1,7 @@
deleteTags(array('variables' => TRUE));
+ cache_clear_all('variables', 'cache');
+}
+
+/**
+ * Delete "extlink_target_value" variable (now uses "extlink_target" directly).
+ */
+function extlink_update_7101(&$sandbox) {
+ variable_del('extlink_target_value');
}
diff --git a/sites/all/modules/contrib/theming/extlink/extlink.js b/sites/all/modules/contrib/theming/extlink/extlink.js
index 4ec65680..dadcf381 100644
--- a/sites/all/modules/contrib/theming/extlink/extlink.js
+++ b/sites/all/modules/contrib/theming/extlink/extlink.js
@@ -1,20 +1,27 @@
(function ($) {
-function extlinkAttach(context) {
+Drupal.extlink = Drupal.extlink || {};
+
+Drupal.extlink.attach = function (context, settings) {
+ if (!settings.hasOwnProperty('extlink')) {
+ return;
+ }
+
// Strip the host name down, removing ports, subdomains, or www.
var pattern = /^(([^\/:]+?\.)*)([^\.:]{4,})((\.[a-z]{1,4})*)(:[0-9]{1,5})?$/;
var host = window.location.host.replace(pattern, '$3$4');
var subdomain = window.location.host.replace(pattern, '$1');
// Determine what subdomains are considered internal.
- if (Drupal.settings.extlink.extSubdomains) {
- var subdomains = "([^/]*\\.)?";
+ var subdomains;
+ if (settings.extlink.extSubdomains) {
+ subdomains = "([^/]*\\.)?";
}
else if (subdomain == 'www.' || subdomain == '') {
- var subdomains = "(www\\.)?";
+ subdomains = "(www\\.)?";
}
else {
- var subdomains = subdomain.replace(".", "\\.");
+ subdomains = subdomain.replace(".", "\\.");
}
// Build regular expressions that define an internal link.
@@ -22,29 +29,29 @@ function extlinkAttach(context) {
// Extra internal link matching.
var extInclude = false;
- if (Drupal.settings.extlink.extInclude) {
- extInclude = new RegExp(Drupal.settings.extlink.extInclude.replace(/\\/, '\\'));
+ if (settings.extlink.extInclude) {
+ extInclude = new RegExp(settings.extlink.extInclude.replace(/\\/, '\\'), "i");
}
// Extra external link matching.
var extExclude = false;
- if (Drupal.settings.extlink.extExclude) {
- extExclude = new RegExp(Drupal.settings.extlink.extExclude.replace(/\\/, '\\'));
+ if (settings.extlink.extExclude) {
+ extExclude = new RegExp(settings.extlink.extExclude.replace(/\\/, '\\'), "i");
}
// Extra external link CSS selector exclusion.
var extCssExclude = false;
- if (Drupal.settings.extlink.extCssExclude) {
- extCssExclude = Drupal.settings.extlink.extCssExclude;
+ if (settings.extlink.extCssExclude) {
+ extCssExclude = settings.extlink.extCssExclude;
}
// Extra external link CSS selector explicit.
var extCssExplicit = false;
- if (Drupal.settings.extlink.extCssExplicit) {
- extCssExplicit = Drupal.settings.extlink.extCssExplicit;
+ if (settings.extlink.extCssExplicit) {
+ extCssExplicit = settings.extlink.extCssExplicit;
}
- // Find all links which are NOT internal and begin with http (as opposed
+ // Find all links which are NOT internal and begin with http as opposed
// to ftp://, javascript:, etc. other kinds of links.
// When operating on the 'this' variable, the host has been appended to
// all links by the browser, even local ones.
@@ -52,75 +59,103 @@ function extlinkAttach(context) {
// available in jQuery 1.0 (Drupal 5 default).
var external_links = new Array();
var mailto_links = new Array();
- $("a:not(." + Drupal.settings.extlink.extClass + ", ." + Drupal.settings.extlink.mailtoClass + "), area:not(." + Drupal.settings.extlink.extClass + ", ." + Drupal.settings.extlink.mailtoClass + ")", context).each(function(el) {
+ $("a:not(." + settings.extlink.extClass + ", ." + settings.extlink.mailtoClass + "), area:not(." + settings.extlink.extClass + ", ." + settings.extlink.mailtoClass + ")", context).each(function(el) {
try {
var url = this.href.toLowerCase();
- if (url.indexOf('http') == 0
- && (!url.match(internal_link) || (extInclude && url.match(extInclude)))
- && !(extExclude && url.match(extExclude))
- && !(extCssExclude && $(this).parents(extCssExclude).length > 0)
- && !(extCssExplicit && $(this).parents(extCssExplicit).length < 1)) {
+ if (url.indexOf('http') == 0
+ && ((!url.match(internal_link) && !(extExclude && url.match(extExclude))) || (extInclude && url.match(extInclude)))
+ && !(extCssExclude && $(this).parents(extCssExclude).length > 0)
+ && !(extCssExplicit && $(this).parents(extCssExplicit).length < 1)) {
external_links.push(this);
}
// Do not include area tags with begin with mailto: (this prohibits
// icons from being added to image-maps).
else if (this.tagName != 'AREA'
- && url.indexOf('mailto:') == 0
- && !(extCssExclude && $(this).parents(extCssExclude).length > 0)
- && !(extCssExplicit && $(this).parents(extCssExplicit).length < 1)) {
+ && url.indexOf('mailto:') == 0
+ && !(extCssExclude && $(this).parents(extCssExclude).length > 0)
+ && !(extCssExplicit && $(this).parents(extCssExplicit).length < 1)) {
mailto_links.push(this);
}
}
// IE7 throws errors often when dealing with irregular links, such as:
// Empty tags.
// example User:pass syntax.
- catch(error) {
+ catch (error) {
return false;
}
});
- if (Drupal.settings.extlink.extClass) {
- // Apply the "ext" class to all links not containing images.
- if (parseFloat($().jquery) < 1.2) {
- $(external_links).not('[img]').addClass(Drupal.settings.extlink.extClass).each(function() { if ($(this).css('display') == 'inline') $(this).after(''); });
- }
- else {
- $(external_links).not($(external_links).find('img').parents('a')).addClass(Drupal.settings.extlink.extClass).each(function() { if ($(this).css('display') == 'inline') $(this).after(''); });
- }
+ if (settings.extlink.extClass) {
+ Drupal.extlink.applyClassAndSpan(external_links, settings.extlink.extClass);
}
- if (Drupal.settings.extlink.mailtoClass) {
- // Apply the "mailto" class to all mailto links not containing images.
- if (parseFloat($().jquery) < 1.2) {
- $(mailto_links).not('[img]').addClass(Drupal.settings.extlink.mailtoClass).each(function() { if ($(this).css('display') == 'inline') $(this).after(''); });
- }
- else {
- $(mailto_links).not($(mailto_links).find('img').parents('a')).addClass(Drupal.settings.extlink.mailtoClass).each(function() { if ($(this).css('display') == 'inline') $(this).after(''); });
- }
+ if (settings.extlink.mailtoClass) {
+ Drupal.extlink.applyClassAndSpan(mailto_links, settings.extlink.mailtoClass);
}
- if (Drupal.settings.extlink.extTarget) {
+ if (settings.extlink.extTarget) {
// Apply the target attribute to all links.
- $(external_links).attr('target', Drupal.settings.extlink.extTarget);
+ $(external_links).attr('target', settings.extlink.extTarget);
}
- if (Drupal.settings.extlink.extAlert) {
- // Add pop-up click-through dialog.
- $(external_links).click(function(e) {
- return confirm(Drupal.settings.extlink.extAlertText);
- });
- }
+ Drupal.extlink = Drupal.extlink || {};
- // Work around for Internet Explorer box model problems.
- if (($.support && !($.support.boxModel === undefined) && !$.support.boxModel) || ($.browser.msie && parseInt($.browser.version) <= 7)) {
- $('span.ext, span.mailto').css('display', 'inline-block');
- }
-}
+ // Set up default click function for the external links popup. This should be
+ // overridden by modules wanting to alter the popup.
+ Drupal.extlink.popupClickHandler = Drupal.extlink.popupClickHandler || function() {
+ if (settings.extlink.extAlert) {
+ return confirm(settings.extlink.extAlertText);
+ }
+ }
-Drupal.behaviors.extlink = {
- attach: function(context){
+ $(external_links).click(function(e) {
+ return Drupal.extlink.popupClickHandler(e);
+ });
+};
+
+/**
+ * Apply a class and a trailing to all links not containing images.
+ *
+ * @param links
+ * An array of DOM elements representing the links.
+ * @param class_name
+ * The class to apply to the links.
+ */
+Drupal.extlink.applyClassAndSpan = function (links, class_name) {
+ var $links_to_process;
+ if (Drupal.settings.extlink.extImgClass){
+ $links_to_process = $(links);
+ }
+ else {
+ var links_with_images = $(links).find('img').parents('a');
+ $links_to_process = $(links).not(links_with_images);
+ }
+ $links_to_process.addClass(class_name);
+ var i;
+ var length = $links_to_process.length;
+ for (i = 0; i < length; i++) {
+ var $link = $($links_to_process[i]);
+ if ($link.css('display') == 'inline' || $link.css('display') == 'inline-block') {
+ if (class_name == Drupal.settings.extlink.mailtoClass) {
+ $link.append(' ' + Drupal.settings.extlink.mailtoLabel + '');
+ }
+ else {
+ $link.append(' ' + Drupal.settings.extlink.extLabel + '');
+ }
+ }
+ }
+};
+
+Drupal.behaviors.extlink = Drupal.behaviors.extlink || {};
+Drupal.behaviors.extlink.attach = function (context, settings) {
+ // Backwards compatibility, for the benefit of modules overriding extlink
+ // functionality by defining an "extlinkAttach" global function.
+ if (typeof extlinkAttach === 'function') {
extlinkAttach(context);
}
-}
+ else {
+ Drupal.extlink.attach(context, settings);
+ }
+};
})(jQuery);
diff --git a/sites/all/modules/contrib/theming/extlink/extlink.module b/sites/all/modules/contrib/theming/extlink/extlink.module
index f9bd7699..fcaae5b5 100644
--- a/sites/all/modules/contrib/theming/extlink/extlink.module
+++ b/sites/all/modules/contrib/theming/extlink/extlink.module
@@ -1,5 +1,7 @@
array(
'extTarget' => variable_get('extlink_target', 0),
'extClass' => variable_get('extlink_class', 'ext'),
+ 'extLabel' => check_plain(variable_get('extlink_label', t('(link is external)'))),
+ 'extImgClass' => variable_get('extlink_img_class', 0),
'extSubdomains' => variable_get('extlink_subdomains', 1),
'extExclude' => variable_get('extlink_exclude', ''),
'extInclude' => variable_get('extlink_include', ''),
@@ -29,8 +33,9 @@ function extlink_page_build() {
'extCssExplicit' => variable_get('extlink_css_explicit', ''),
'extAlert' => variable_get('extlink_alert', 0),
'extAlertText' => variable_get('extlink_alert_text', 'This link will take you to an external web site. We are not responsible for their content.'),
- 'mailtoClass' => variable_get('extlink_mailto_class', 'mailto'))), 'setting'
- );
+ 'mailtoClass' => variable_get('extlink_mailto_class', 'mailto'),
+ 'mailtoLabel' => check_plain(variable_get('extlink_mailto_label', t('(link sends e-mail)'))),
+ )), 'setting');
}
function extlink_admin_settings() {
@@ -46,12 +51,20 @@ function extlink_admin_settings() {
$form['extlink_mailto_class'] = array(
'#type' => 'checkbox',
- '#title' => t('Place an icon next to mailto links'),
+ '#title' => t('Place an icon next to mailto links.'),
'#return_value' => 'mailto',
'#default_value' => variable_get('extlink_mailto_class', 'mailto'),
'#description' => t('Places an !icon icon next to mailto links.', array('!icon' => theme('image',array('path' => drupal_get_path('module', 'extlink') . '/mailto.png', 'alt' => t('Email links icon'))))),
);
+ $form['extlink_img_class'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Place an icon next to image links.'),
+ '#return_value' => TRUE,
+ '#default_value' => variable_get('extlink_img_class', FALSE),
+ '#description' => t('If checked, images wrapped in an anchor tag will be treated as external links.'),
+ );
+
$form['extlink_subdomains'] = array(
'#type' => 'checkbox',
'#title' => t('Exclude links with the same primary domain.'),
@@ -61,7 +74,7 @@ function extlink_admin_settings() {
$form['extlink_target'] = array(
'#type' => 'checkbox',
- '#title' => t('Open external links in a new window'),
+ '#title' => t('Open external links in a new window.'),
'#return_value' => '_blank',
'#default_value' => variable_get('extlink_target', 0),
);
@@ -88,16 +101,16 @@ function extlink_admin_settings() {
);
$patterns = array(
- '(example\.com) ' . t('Matches example.com.'),
- '(example\.com)|(example\.net) ' . t('Multiple patterns can be strung together by using a pipe. Matches example.com OR example.net.'),
- '(links/goto/[0-9]+/[0-9]+) ' . t('Matches links that go through the Links module redirect.'),
+ '(example\.com)
' . t('Matches example.com.'),
+ '(example\.com)|(example\.net)
' . t('Multiple patterns can be strung together by using a pipe. Matches example.com OR example.net.'),
+ '(links/goto/[0-9]+/[0-9]+)
' . t('Matches links that go through the Links module redirect.'),
);
$wildcards = array(
- '. ' . t('Matches any character.'),
- '? ' . t('The previous character or set is optional.'),
- '\d ' . t('Matches any digit (0-9).'),
- '[a-z] ' . t('Brackets may be used to match a custom set of characters. This matches any alphabetic letter.'),
+ '.
' . t('Matches any character.'),
+ '?
' . t('The previous character or set is optional.'),
+ '\d
' . t('Matches any digit (0-9).'),
+ '[a-z]
' . t('Brackets may be used to match a custom set of characters. This matches any alphabetic letter.'),
);
$form['patterns'] = array(
@@ -112,7 +125,7 @@ function extlink_admin_settings() {
theme('item_list', array('items' => $patterns)) .
t('Common special characters:') .
theme('item_list', array('items' => $wildcards)) .
- '' . t('All special characters (^ $ . ? ( ) | * +) must also be escaped with backslashes. Patterns are not case-sensitive. Any pattern supported by JavaScript may be used.') . '
',
+ '' . t('All special characters (!characters) must also be escaped with backslashes. Patterns are not case-sensitive. Any pattern supported by JavaScript may be used.', array('!characters' => '^ $ . ? ( ) | * +
')) . '
',
);
$form['patterns']['extlink_exclude'] = array(
diff --git a/sites/all/modules/contrib/theming/extlink/extlink.test b/sites/all/modules/contrib/theming/extlink/extlink.test
index 728dc794..2459125b 100644
--- a/sites/all/modules/contrib/theming/extlink/extlink.test
+++ b/sites/all/modules/contrib/theming/extlink/extlink.test
@@ -1,25 +1,97 @@
normal_user = $this->drupalCreateUser($permissions);
+
+ // Create an admin user.
+ $permissions[] = 'administer site configuration';
+ $permissions[] = 'administer permissions';
+ $permissions[] = 'administer content types';
+ $this->admin_user = $this->drupalCreateUser($permissions);
+ }
+
+ protected function getNodeFormValues() {
+ $edit = array(
+ 'title' => 'node_title ' . $this->randomName(32),
+ 'body[' . LANGUAGE_NONE . '][0][value]' => 'node_body ' . $this->randomName(256) . ' Google!',
+ );
+ return $edit;
+ }
+
+ /**
+ * Test if External Link is present
+ */
+ protected function assertExternalLinkPresence() {
+ $elements = $this->xpath('//span[@class="ext"]');
+ if (count($elements) > 0)
+ $this->pass('There should be an External Link on the form.', 'External Links');
+ else
+ $this->fail('There should be an External Link on the form.', 'External Links');
+ }
+}
+
+class ExtlinkTestCase extends ExtlinkBaseWebTestCase {
public static function getInfo() {
- return array (
- 'name' => t('Administration'),
- 'description' => t('Administration tests for my module.'),
+ return array(
+ 'name' => t('General External Links functionality'),
+ 'description' => t('Testing the basic functionality of External Links'),
'group' => t('External Links'),
);
}
- public function
+}
-setUp() {
- // Enable any module that you will need in your tests.
- parent::setUp();
+class ExtlinkAdminTestCase extends ExtlinkBaseWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => t('External Links administration functionality'),
+ 'description' => t('Testing of the External Links administration interface and functionality.'),
+ 'group' => t('External Links'),
+ );
+ }
+
+ /**
+ * Test access to the admin pages.
+ */
+ function testAdminAccess() {
+ $this->drupalLogin($this->normal_user);
+ $this->drupalGet(self::EXTLINK_ADMIN_PATH);
+ file_put_contents('tmp.simpletest.html', $this->drupalGetContent());
+ $this->assertText(t('Access denied'), 'Normal users should not be able to access the External Links admin pages', 'External Links');
+
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet(self::EXTLINK_ADMIN_PATH);
+ file_put_contents('tmp.simpletest.html', $this->drupalGetContent());
+ $this->assertNoText(t('Access denied'), 'Admin users should be able to access the External Links admin pages', 'External Links');
}
}
?>
\ No newline at end of file
diff --git a/sites/all/modules/contrib/theming/extlink/extlink_s.png b/sites/all/modules/contrib/theming/extlink/extlink_s.png
new file mode 100644
index 00000000..778fb586
Binary files /dev/null and b/sites/all/modules/contrib/theming/extlink/extlink_s.png differ
diff --git a/sites/all/modules/contrib/users/email_registration/README.txt b/sites/all/modules/contrib/users/email_registration/README.txt
index 38496b6f..668681dd 100644
--- a/sites/all/modules/contrib/users/email_registration/README.txt
+++ b/sites/all/modules/contrib/users/email_registration/README.txt
@@ -34,11 +34,8 @@ $conf['locale_custom_strings_en'][''] = array(
);
-
-
BUGS, FEATURES, QUESTIONS
=========================
Post any bugs, features or questions to the issue queue:
http://drupal.org/project/issues/email_registration
-
diff --git a/sites/all/modules/contrib/users/email_registration/email_registration.api.php b/sites/all/modules/contrib/users/email_registration/email_registration.api.php
index b38b78fe..19d7e2e6 100644
--- a/sites/all/modules/contrib/users/email_registration/email_registration.api.php
+++ b/sites/all/modules/contrib/users/email_registration/email_registration.api.php
@@ -2,7 +2,7 @@
/**
* @file
- * Documentation for email_registration API.
+ * Documentation for email_registration module API.
*/
/**
@@ -17,16 +17,19 @@
* to generate a username (return a string to be used as the username, NULL
* to have email_registration generate it).
*
- * @param $edit
+ * @param array $edit
* The array of form values submitted by the user.
- * @param $account
+ * @param object $account
* The user object on which the operation is being performed.
*
- * @return
+ * @return string
* A string defining a generated username.
*/
function hook_email_registration_name($edit, $account) {
- return 'u' . $account->uid;
+ // Your hook implementation should ensure that the resulting string
+ // works as a username. You can use email_registration_cleanup_username($name)
+ // to clean up the name.
+ return email_registration_cleanup_username('u' . $account->uid);
}
/**
diff --git a/sites/all/modules/contrib/users/email_registration/email_registration.info b/sites/all/modules/contrib/users/email_registration/email_registration.info
index f9d99200..82afb1fa 100644
--- a/sites/all/modules/contrib/users/email_registration/email_registration.info
+++ b/sites/all/modules/contrib/users/email_registration/email_registration.info
@@ -3,9 +3,9 @@ description = Allows users to register with an e-mail address as their username.
files[] = email_registration.test
core = 7.x
-; Information added by drupal.org packaging script on 2013-01-19
-version = "7.x-1.1"
+; Information added by Drupal.org packaging script on 2014-04-23
+version = "7.x-1.2"
core = "7.x"
project = "email_registration"
-datestamp = "1358554303"
+datestamp = "1398265775"
diff --git a/sites/all/modules/contrib/users/email_registration/email_registration.module b/sites/all/modules/contrib/users/email_registration/email_registration.module
index 94423a49..65c34cc2 100644
--- a/sites/all/modules/contrib/users/email_registration/email_registration.module
+++ b/sites/all/modules/contrib/users/email_registration/email_registration.module
@@ -24,6 +24,8 @@ function email_registration_user_insert(&$edit, &$account, $category = NULL) {
if (empty($names)) {
// Strip off everything after the @ sign.
$new_name = preg_replace('/@.*$/', '', $edit['mail']);
+ // Clean up the username.
+ $new_name = email_registration_cleanup_username($new_name, $account->uid);
}
else {
// One would expect a single implementation of the hook, but if there
@@ -42,46 +44,30 @@ function email_registration_user_insert(&$edit, &$account, $category = NULL) {
$edit['name'] = $new_name;
$account->name = $new_name;
- return;
}
/**
- * Given a starting point for a Drupal username (e.g. the name portion of an
- * email address) return a legal, unique Drupal username. This function is
- * designed to work on the results of the /user/register or /admin/people/create
- * forms which have already called user_validate_name, valid_email_address
- * or a similar function. If your custom code is creating users, you should
- * ensure that the email/name is already validated using something like that.
+ * Given a starting point returns a legal, unique Drupal username.
*
- * @param $name
- * A name from which to base the final user name. May contain illegal characters; these will be stripped.
+ * This function is designed to work on the results of the /user/register or
+ * /admin/people/create forms which have already called user_validate_name,
+ * valid_email_address or a similar function. If your custom code is creating
+ * users, you should ensure that the email/name is already validated using
+ * something like that.
*
- * @param $uid
- * (optional) Uid to ignore when searching for unique user (e.g. if we update the username after the
- * {users} row is inserted)
+ * @param string $name
+ * A name from which to base the final user name. May contain illegal
+ * characters; these will be stripped.
+ * @param int $uid
+ * (optional) Uid to ignore when searching for unique user
+ * (e.g. if we update the username after the {users} row is inserted)
*
- * @return
+ * @return string
* A unique user name based on $name.
*
- * @see user_validate_name().
- *
+ * @see user_validate_name()
*/
-function email_registration_unique_username($name, $uid = 0) {
- // Strip illegal characters.
- $name = preg_replace('/[^\x{80}-\x{F7} a-zA-Z0-9@_.\'-]/', '', $name);
-
- // Strip leading and trailing spaces.
- $name = trim($name);
-
- // Convert any other series of spaces to a single underscore.
- $name = preg_replace('/ +/', '_', $name);
-
- // If there's nothing left use a default.
- $name = ('' === $name) ? t('user') : $name;
-
- // Truncate to reasonable size.
- $name = (drupal_strlen($name) > (USERNAME_MAX_LENGTH - 10)) ? drupal_substr($name, 0, USERNAME_MAX_LENGTH - 11) : $name;
-
+function email_registration_unique_username($name, $uid) {
// Iterate until we find a unique name.
$i = 0;
do {
@@ -93,12 +79,47 @@ function email_registration_unique_username($name, $uid = 0) {
return $new_name;
}
+/**
+ * Function to clean up username.
+ *
+ * e.g.
+ * Replace two or more spaces with a single underscore
+ * Strip illegal characters.
+ *
+ * @param string $name
+ * The username to be cleaned up.
+ *
+ * @return string
+ * Cleaned up username.
+ */
+function email_registration_cleanup_username($name, $uid = NULL) {
+ // Strip illegal characters.
+ $name = preg_replace('/[^\x{80}-\x{F7} a-zA-Z0-9@_.\'-]/', '', $name);
+
+ // Strip leading and trailing spaces.
+ $name = trim($name);
+
+ // Convert any other series of spaces to a single underscore.
+ $name = preg_replace('/ +/', '_', $name);
+
+ // If there's nothing left use a default.
+ $name = ('' === $name) ? t('user') : $name;
+
+ if (!empty($uid)) {
+ // Put uid on the end of the name.
+ $name = $name . '_' . $uid;
+ }
+
+ // Truncate to a reasonable size.
+ $name = (drupal_strlen($name) > (USERNAME_MAX_LENGTH - 10)) ? drupal_substr($name, 0, USERNAME_MAX_LENGTH - 11) : $name;
+ return $name;
+}
/**
* Implements hook_form_FORM_ID_alter().
*/
function email_registration_form_user_register_form_alter(&$form, &$form_state) {
- $form['account']['name']['#type'] = 'value';
+ $form['account']['name']['#type'] = 'hidden';
$form['account']['name']['#value'] = 'email_registration_' . user_password();
$form['account']['mail']['#title'] = t('E-mail');
}
@@ -108,7 +129,6 @@ function email_registration_form_user_register_form_alter(&$form, &$form_state)
*/
function email_registration_form_user_pass_alter(&$form, &$form_state) {
$form['name']['#title'] = t('E-mail');
- $form['name']['#description'] = t('A password reset message will be sent to your e-mail address.');
}
/**
@@ -131,11 +151,12 @@ function email_registration_form_user_login_block_alter(&$form, &$form_state) {
/**
* Form element validation handler for the user login form.
+ *
* Allows users to authenticate by email, which is our preferred method.
*/
function email_registration_user_login_validate($form, &$form_state) {
if (isset($form_state['values']['name'])) {
- // Keep the email value in form state for furher validation.
+ // Keep the email value in form state for further validation.
$form_state['values']['email'] = $form_state['values']['name'];
if ($name = db_query('SELECT name FROM {users} WHERE LOWER(mail) = LOWER(:name)', array(':name' => $form_state['values']['name']))->fetchField()) {
$form_state['values']['name'] = $name;
@@ -146,6 +167,6 @@ function email_registration_user_login_validate($form, &$form_state) {
/**
* Implements hook_form_FORM_ID_alter().
*/
-function email_registration_form_user_profile_form_alter($form, &$form_state) {
+function email_registration_form_user_profile_form_alter(&$form, &$form_state) {
$form['account']['name']['#title'] = t('Display name');
}
diff --git a/sites/all/modules/contrib/users/email_registration/email_registration.test b/sites/all/modules/contrib/users/email_registration/email_registration.test
index 55187af6..3362b80f 100644
--- a/sites/all/modules/contrib/users/email_registration/email_registration.test
+++ b/sites/all/modules/contrib/users/email_registration/email_registration.test
@@ -2,7 +2,7 @@
/**
* @file
- * email registration simpletest
+ * Contains EmailRegistrationTestCase.
*/
class EmailRegistrationTestCase extends DrupalWebTestCase {
@@ -20,7 +20,7 @@ class EmailRegistrationTestCase extends DrupalWebTestCase {
/**
* Implementation of setUp().
*/
- function setUp() {
+ public function setUp() {
parent::setUp('email_registration');
// Configure to allow set password.
@@ -30,7 +30,7 @@ class EmailRegistrationTestCase extends DrupalWebTestCase {
/**
* Test various behaviors for anonymous users.
*/
- function testRegistration() {
+ public function testRegistration() {
variable_set('user_register', USER_REGISTER_VISITORS);
// Try to register a user.
$name = $this->randomName();
@@ -49,8 +49,12 @@ class EmailRegistrationTestCase extends DrupalWebTestCase {
);
$this->drupalPost('user/login', $login, t('Log in'));
- // Really basic confirmation that the user was created and logged in.
- $this->assertRaw('' . $name . ' | Drupal ', t('User properly created, logged in.'));
+ // Get the uid.
+ $accounts = user_load_multiple(array(), array('mail' => $name . '@example.com'));
+ $new_user = reset($accounts);
+
+ // Confirm the user was created and logged in with expected username.
+ $this->assertRaw('' . $name . '_' . $new_user->uid . ' | Drupal ', t('User properly created, logged in.'));
// Now try the immediate login.
$this->drupalLogout();
@@ -64,6 +68,6 @@ class EmailRegistrationTestCase extends DrupalWebTestCase {
);
$this->drupalPost('/user/register', $register, t('Create new account'));
$this->assertRaw('Registration successful. You are now logged in.', t('User properly created, immediately logged in.'));
-
}
+
}
diff --git a/sites/all/modules/contrib/users/faq/.gitignore b/sites/all/modules/contrib/users/faq/.gitignore
new file mode 100644
index 00000000..a0238825
--- /dev/null
+++ b/sites/all/modules/contrib/users/faq/.gitignore
@@ -0,0 +1,4 @@
+*.patch
+*.diff
+.idea/
+.idea/*
diff --git a/sites/all/modules/contrib/users/faq/faq.admin.inc b/sites/all/modules/contrib/users/faq/faq.admin.inc
index 72cccac2..9409a8d9 100644
--- a/sites/all/modules/contrib/users/faq/faq.admin.inc
+++ b/sites/all/modules/contrib/users/faq/faq.admin.inc
@@ -8,15 +8,15 @@
/**
* Generates the settings form for the FAQ module.
*
- * @param $op
+ * @param string $op
* Default value is NULL; determines what are the permissions of the current
* user on the FAQ.
- * @return
+ *
+ * @return string
* The output, which contains the HTML code for the settings form generated by
* drupal_get_form() function.
*/
function faq_settings_page($op = NULL) {
-
$output = drupal_get_form('faq_general_settings_form');
return $output;
@@ -25,7 +25,7 @@ function faq_settings_page($op = NULL) {
/**
* Define a form to edit the page header and descriptive text.
*
- * @return
+ * @return array
* The general settings form code stored in the $form variable, before
* converted to HTML.
*/
@@ -48,7 +48,12 @@ function faq_general_settings_form($form) {
$form['faq_custom_breadcrumbs'] = array(
'#type' => 'checkbox',
'#title' => t('Create custom breadcrumbs for the FAQ'),
- '#description' => t('This option set the breadcrumb path to "%home > %faqtitle > category trail".', array('%home' => t('Home'), '%faqtitle' => variable_get('faq_title', 'Frequently Asked Questions'))),
+ '#description' => t('This option set the breadcrumb path to "%home > %faqtitle > category trail".',
+ array(
+ '%home' => t('Home'),
+ '%faqtitle' => variable_get('faq_title', 'Frequently Asked Questions'),
+ )
+ ),
'#default_value' => variable_get('faq_custom_breadcrumbs', TRUE),
);
@@ -58,7 +63,7 @@ function faq_general_settings_form($form) {
/**
* Define the elements for the FAQ Settings page - Questions tab.
*
- * @return
+ * @return array
* The form code inside the $form array.
*/
function faq_questions_settings_form($form, &$form_state) {
@@ -259,7 +264,7 @@ function faq_questions_settings_form($form, &$form_state) {
/**
* Define the elements for the FAQ Settings page - categories tab.
*
- * @return
+ * @return array
* The form code inside the $form array.
*/
function faq_categories_settings_form($form, &$form_state) {
@@ -459,9 +464,10 @@ function faq_categories_settings_form_submit($form, &$form_state) {
/**
* Define the elements for the FAQ Settings page - order tab.
*
- * @param $form_state
+ * @param array $form_state
* Store the submitted form values.
- * @return
+ *
+ * @return array
* The form code, before being converted to HTML format.
*/
function faq_order_settings_form($form, $form_state, $category = NULL) {
@@ -541,7 +547,7 @@ function faq_order_settings_form($form, $form_state, $category = NULL) {
// $default_weight is an integer.
$query->addExpression("COALESCE(w.weight, $default_weight)", 'effective_weight');
// Doesn't work in Postgres.
- //$query->addExpression('COALESCE(w.weight, CAST(:default_weight as SIGNED))', 'effective_weight', array(':default_weight' => $default_weight));
+ // $query->addExpression('COALESCE(w.weight, CAST(:default_weight as SIGNED))', 'effective_weight', array(':default_weight' => $default_weight));
if (empty($category)) {
$category = 0;
@@ -594,16 +600,16 @@ function faq_order_settings_form($form, $form_state, $category = NULL) {
);
}
-
return $form;
}
/**
* Function set the rebuild of the form in the FAQ Settings - Weight tab.
*
- * @param $form
+ * @param array $form
* Array, containing the form structure.
- * @param &$form_state
+ *
+ * @param array &$form_state
* The 'rebuild' key inside $form_state['rebuild'] structure, overrides the
* 'redirect' key: when it is set to TRUE, the form will be rebuilt from
* scratch and displayed on screen.
@@ -615,9 +621,10 @@ function faq_order_settings_choose_cat_form_submit($form, &$form_state) {
/**
* Save the options set by the user in the FAQ Settings - Weight tab.
*
- * @param $form
+ * @param array $form
* Array, containing the form structure.
- * @param &$form_state
+ *
+ * @param array &$form_state
* $form_state['values'] stores the submitted values from the form.
*/
function faq_order_settings_reorder_form_submit($form, &$form_state) {
@@ -663,4 +670,3 @@ function faq_order_settings_reorder_form_submit($form, &$form_state) {
drupal_set_message(t('Configuration has been updated.'));
}
}
-
diff --git a/sites/all/modules/contrib/users/faq/faq.info b/sites/all/modules/contrib/users/faq/faq.info
index f6a82084..8ec42bbb 100644
--- a/sites/all/modules/contrib/users/faq/faq.info
+++ b/sites/all/modules/contrib/users/faq/faq.info
@@ -20,11 +20,11 @@ files[] = includes/faq.new_page.inc
files[] = includes/faq.questions_inline.inc
files[] = includes/faq.questions_top.inc
files[] = views/faq.views.inc
-configure = /admin/config/content/faq
+configure = admin/config/content/faq
-; Information added by drupal.org packaging script on 2013-09-30
-version = "7.x-1.0-rc2+8-dev"
+; Information added by Drupal.org packaging script on 2015-03-27
+version = "7.x-1.0"
core = "7.x"
project = "faq"
-datestamp = "1380577865"
+datestamp = "1427455703"
diff --git a/sites/all/modules/contrib/users/faq/faq.install b/sites/all/modules/contrib/users/faq/faq.install
index e83c35bf..5b8510d5 100644
--- a/sites/all/modules/contrib/users/faq/faq.install
+++ b/sites/all/modules/contrib/users/faq/faq.install
@@ -8,7 +8,7 @@
/**
* Define the 'faq_weights' and 'faq_questions' table structures.
*
- * @return
+ * @return array
* The schema which contains the structure for the faq module's tables.
*/
function faq_schema() {
@@ -73,7 +73,6 @@ function faq_schema() {
'primary key' => array('nid', 'vid'),
);
-
return $schema;
}
@@ -85,6 +84,7 @@ function faq_schema() {
function faq_install() {
variable_set('node_type_faq', array('status'));
$t = get_t();
+
// Ensure the FAQ node type is available.
node_types_rebuild();
$types = node_type_get_types();
@@ -93,7 +93,13 @@ function faq_install() {
// Change the default label on the body field.
$body_instance = field_info_instance('node', 'body', 'faq');
$body_instance['label'] = $t('Answer');
+
field_update_instance($body_instance);
+
+ // Add the detailed question field.
+ _faq_add_custom_fields();
+ // Shift all fields below the body field one down and put detailed question field where the body field was.
+ _faq_shift_fields_down();
}
/**
@@ -136,12 +142,38 @@ function faq_uninstall() {
// Block settings.
variable_del('faq_block_recent_faq_count');
variable_del('faq_block_random_faq_count');
- // Custom breadcrumbs control
+ // Custom breadcrumbs control.
variable_del('faq_custom_breadcrumbs');
// Deprecated.
variable_del('faq_more_link');
+ // Remove content type and the fields created.
+ $faq_type = 'faq';
+ $sql = 'SELECT nid FROM {node} n WHERE n.type = :type';
+ $result = db_query($sql, array(':type' => $faq_type));
+ $nodeids = array();
+ foreach ($result as $row) {
+ $nodeids[] = $row->nid;
+ }
+ node_delete_multiple($nodeids);
+ _faq_delete_custom_fields();
+ node_type_delete($faq_type);
+ field_purge_batch(500);
+
+ // Remove content type and the fields created.
+ $faq_type = 'faq';
+ $sql = 'SELECT nid FROM {node} n WHERE n.type = :type';
+ $result = db_query($sql, array(':type' => $faq_type));
+ $nodeids = array();
+ foreach ($result as $row) {
+ $nodeids[] = $row->nid;
+ }
+ node_delete_multiple($nodeids);
+ _faq_delete_custom_fields();
+ node_type_delete($faq_type);
+ field_purge_batch(500);
+
// Clear the cache tables.
cache_clear_all('*', 'cache', TRUE);
cache_clear_all('*', 'cache_filter', TRUE);
@@ -237,7 +269,13 @@ function faq_update_2() {
*/
function faq_update_6003() {
$ret = array();
- db_add_field('faq_questions', 'detailed_question', array('type' => 'text', 'size' => 'normal', 'not null' => TRUE));
+ db_add_field('faq_questions', 'detailed_question',
+ array(
+ 'type' => 'text',
+ 'size' => 'normal',
+ 'not null' => TRUE,
+ )
+ );
db_update('faq_questions')
->expression('detailed_question', 'question')
@@ -250,7 +288,13 @@ function faq_update_6003() {
* Make'detailed_question' column nullable.
*/
function faq_update_7000() {
- db_change_field('faq_questions', 'detailed_question', 'detailed_question', array('type' => 'text', 'size' => 'normal', 'not null' => FALSE));
+ db_change_field('faq_questions', 'detailed_question', 'detailed_question',
+ array(
+ 'type' => 'text',
+ 'size' => 'normal',
+ 'not null' => FALSE,
+ )
+ );
return t('Detailed question field can now be null.');
}
@@ -264,3 +308,288 @@ function faq_update_7001() {
return t('Deleted obsolete variables.');
}
+/**
+ * Convert old-style detailed questions to new fields.
+ */
+function faq_update_7002(&$sandbox) {
+ // Number of nodes to update each pass.
+ define('BATCH_SIZE_7002', '5');
+
+ // Do this the first time.
+ if (!isset($sandbox['progress'])) {
+
+ // Initialize sandbox structure for multi-pass update.
+ $sandbox['progress'] = 0;
+ $sandbox['current_idx'] = 0;
+
+ // Get faq nodes and run the query as user 1.
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'node')
+ ->entityCondition('bundle', 'faq')
+ ->addMetaData('account', user_load(1));
+ $result = $query->execute();
+
+ if (isset($result['node'])) {
+ $sandbox['faq_items_nids'] = array_keys($result['node']);
+ $sandbox['max'] = count($sandbox['faq_items_nids']);
+ }
+ else {
+ $sandbox['faq_items_nids'] = array();
+ $sandbox['max'] = 0;
+ }
+
+ // Add the detailed question field.
+ _faq_add_custom_fields();
+
+ // Adjust the weight of the field so that it is above the answer (body).
+ _faq_shift_fields_down();
+ }
+
+ $count = 0;
+ // Convert old-style detailed questions to new full field.
+ while (($nid = $sandbox['faq_items_nids'][$sandbox['current_idx']]) && $count < BATCH_SIZE_7002) {
+
+ // Load the full node to be updated.
+ $node = node_load($nid);
+
+ // Load the detailed question.
+ $dq = isset($node->detailed_question) ? $node->detailed_question : '';
+ if ($dq == '') {
+ $select = db_select('faq_questions', 'f');
+ $dq = $select->condition('f.nid', $node->nid)->fields('f', array('detailed_question'))->execute()->fetchField();
+ }
+
+ // Get the default text filter format from DB as this might be integer if upgraded site or tekststring if new D7 site.
+ // Default filter format: Filtered HTML.
+ $filter_formats = filter_formats();
+ $filter_formats_keys = array_keys($filter_formats);
+ $filter_format = reset($filter_formats_keys);
+
+ // Get the language(s) from the body, making sure we have the same set for detailed question too.
+ $langs = array_keys($node->body);
+
+ // Add proper taxonomy fields.
+ $txonselect = db_select('taxonomy_index', 't');
+ $taxres = $txonselect->fields('t', array('tid'))->condition('t.nid', $node->nid)->execute();
+ foreach ($taxres as $taxon) {
+ $term = taxonomy_term_load($taxon->tid);
+ $vocab = taxonomy_vocabulary_load($term->vid);
+
+ foreach ($langs as $language) {
+ // Find out if there is a field added with the vocabulary of this term.
+ if (isset($node->{$vocab->module . "_" . $vocab->machine_name})) {
+ $node->{$vocab->module . "_" . $vocab->machine_name}[$language][$term->tid] = (array) $term;
+ }
+ }
+ }
+
+ // Add detailed question field for all languages.
+ foreach ($langs as $language) {
+ $node->field_detailed_question[$language][0]['value'] = $dq;
+ $node->field_detailed_question[$language][0]['format'] = $filter_format;
+ $node->field_detailed_question[$language][0]['safe_value'] = check_markup($dq, $filter_format, $language);
+ }
+
+ // Save resulting node.
+ node_save($node);
+
+ // Should not be more than BATCH_SIZE_7002.
+ $count++;
+ // Progress counter.
+ $sandbox['progress']++;
+ // Node array index pointer.
+ $sandbox['current_idx']++;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+
+ return t('Custom field added, @count questions converted into fields.', array('@count' => $sandbox['max'] + 1));
+}
+
+/**
+ * Code examples modified.
+ *
+ * @see http://www.sitepoint.com/creating-a-new-drupal-node-type/
+ * @see http://www.thecarneyeffect.co.uk/creating-custom-content-type-adding-fields-programmatically-drupal-7
+ */
+function _faq_add_custom_fields() {
+ foreach (_faq_installed_fields() as $field) {
+ field_create_field($field);
+ }
+ foreach (_faq_installed_instances() as $fieldinstance) {
+ $fieldinstance['entity_type'] = 'node';
+ $fieldinstance['bundle'] = 'faq';
+ field_create_instance($fieldinstance);
+ }
+}
+
+/**
+ * Return the detailed question field definition.
+ */
+function _faq_installed_fields() {
+ $t = get_t();
+ return array(
+ 'detailed_question' => array(
+ 'translatable' => '0',
+ 'entity_types' => array(),
+ 'settings' => array(),
+ 'storage' => array(
+ 'type' => 'field_sql_storage',
+ 'settings' => array(),
+ 'module' => 'field_sql_storage',
+ 'active' => '1',
+ 'details' => array(
+ 'sql' => array(
+ 'FIELD_LOAD_CURRENT' => array(
+ 'field_data_field_detailed_question' => array(
+ 'value' => 'field_detailed_question_value',
+ 'format' => 'field_detailed_question_format',
+ ),
+ ),
+ 'FIELD_LOAD_REVISION' => array(
+ 'field_revision_field_detailed_question' => array(
+ 'value' => 'field_detailed_question_value',
+ 'format' => 'field_detailed_question_format',
+ ),
+ ),
+ ),
+ ),
+ ),
+ 'foreign keys' => array(
+ 'format' => array(
+ 'table' => 'filter_format',
+ 'columns' => array(
+ 'format' => 'format',
+ ),
+ ),
+ ),
+ 'indexes' => array(
+ 'format' => array(
+ 'format',
+ ),
+ ),
+ 'field_name' => 'field_detailed_question',
+ 'type' => 'text_long',
+ 'module' => 'text',
+ 'active' => '1',
+ 'locked' => '0',
+ 'cardinality' => '1',
+ 'deleted' => '0',
+ 'columns' => array(
+ 'value' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ ),
+ 'format' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'bundles' => array(
+ 'node' => array(
+ 'page',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * Returns the detailed question field instance.
+ */
+function _faq_installed_instances() {
+ $t = get_t();
+ // Position in the question and answer page. Body (Answer) is 0 and Question (Title) is -4.
+ return array(
+ 'detailed_question' => array(
+ 'label' => 'Detailed question',
+ 'widget' => array(
+ 'weight' => '-3',
+ 'type' => 'text_textarea',
+ 'module' => 'text',
+ 'active' => 1,
+ 'settings' => array(
+ 'rows' => '5',
+ ),
+ ),
+ 'settings' => array(
+ 'text_processing' => '1',
+ 'user_register_form' => FALSE,
+ ),
+ 'display' => array(
+ 'default' => array(
+ 'label' => 'hidden',
+ 'type' => 'text_default',
+ 'weight' => '-3',
+ 'settings' => array(),
+ 'module' => 'text',
+ ),
+ 'teaser' => array(
+ 'label' => 'hidden',
+ 'type' => 'text_default',
+ 'weight' => '-3',
+ 'settings' => array(),
+ 'module' => 'text',
+ ),
+ ),
+ 'required' => 0,
+ 'description' => 'Enter the detailed question text',
+ 'default_value' => NULL,
+ 'field_name' => 'field_detailed_question',
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ 'deleted' => '0',
+ ),
+ );
+}
+
+/**
+ * Cleanup custom fields on uninstall.
+ */
+function _faq_delete_custom_fields() {
+ foreach (array_keys(_faq_installed_fields()) as $field) {
+ field_delete_field($field);
+ }
+ $instances = field_info_instances('node', 'faq');
+ foreach ($instances as $instance_name => $fieldinstance) {
+ field_delete_instance($fieldinstance);
+ }
+}
+
+/**
+ * Shift fields down.
+ */
+function _faq_shift_fields_down() {
+ // Adjust the weight of the field so that it is above the answer (body).
+ $instance = field_read_instance('node', 'field_detailed_question', 'faq');
+
+ // Get all bundle instances.
+ $instances = field_info_instances('node', 'faq');
+ $body_widget_weight = $instances['body']['widget']['weight'];
+ $body_default_weight = $instances['body']['display']['default']['weight'];
+ $body_teaser_weight = $instances['body']['display']['teaser']['weight'];
+
+ // Move all of them one down so that.
+ foreach ($instances as $field => $settings) {
+ if ($settings['widget']['weight'] >= $body_widget_weight) {
+ $settings['widget']['weight']++;
+ }
+
+ if ($settings['display']['default']['weight'] >= $body_default_weight) {
+ $settings['display']['default']['weight']++;
+ }
+
+ if ($settings['display']['teaser']['weight'] >= $body_teaser_weight) {
+ $settings['display']['teaser']['weight']++;
+ }
+
+ field_update_instance($settings);
+ }
+
+ $instance['widget']['weight'] = $body_widget_weight;
+ $instance['display']['default']['weight'] = $body_default_weight;
+ $instance['display']['teaser']['weight'] = $body_teaser_weight;
+ field_update_instance($instance);
+}
diff --git a/sites/all/modules/contrib/users/faq/faq.js b/sites/all/modules/contrib/users/faq/faq.js
index 84a25554..a4fb073a 100644
--- a/sites/all/modules/contrib/users/faq/faq.js
+++ b/sites/all/modules/contrib/users/faq/faq.js
@@ -67,25 +67,26 @@
$('div.faq-qa-hide', context).hide();
}
$('div.faq-qa-header .faq-header:not(.faq-processed)', context).addClass('faq-processed').click(function() {
+ var $this = $(this);
if (faq_category_hide_qa_accordion) {
- $('div.faq-qa-header .faq-header').not($(this)).removeClass('faq-category-qa-visible');
+ $('.faq-category-qa-visible').not($('.faq-category-qa-visible').closest('.faq-category-group').has($this).children('div.faq-qa-hide')).removeClass('faq-category-qa-visible');
}
$(this).toggleClass('faq-category-qa-visible');
- $('div.faq-qa-hide').not($(this).parent().next('div.faq-qa-hide')).addClass("collapsed");
+ $('div.faq-qa-hide').not($this.parent().siblings('div.faq-qa-hide')).not($('div.faq-qa-hide').closest('.faq-category-group').has($this).children('div.faq-qa-hide')).addClass("collapsed");
if (!faq_category_hide_qa_accordion) {
- $(this).parent().next('div.faq-qa-hide').slideToggle('fast', function() {
+ $this.parent().siblings('div.faq-qa-hide').slideToggle('fast', function() {
$(this).parent().toggleClass('expanded');
});
}
- $(this).parent().next('div.faq-qa-hide').toggleClass("collapsed");
+ $this.parent().siblings('div.faq-qa-hide').toggleClass("collapsed");
// Scroll the page if the collapsed FAQ is not visible.
// If we have the toolbar so the title may be hidden by the bar.
var mainScrollTop = Math.max($('html', context).scrollTop(), $('body', context).scrollTop());
// We compute mainScrollTop because the behaviour is different on FF, IE and CR
- if (mainScrollTop > $(this).offset().top) {
+ if (mainScrollTop > $this.offset().top) {
$('html, body', context).animate({
- scrollTop: $(this).offset().top
+ scrollTop: $this.offset().top
}, 1);
}
@@ -122,7 +123,6 @@
});
});
}
-
}
- }
+ };
})(jQuery);
diff --git a/sites/all/modules/contrib/users/faq/faq.module b/sites/all/modules/contrib/users/faq/faq.module
index 7682926c..82e18096 100644
--- a/sites/all/modules/contrib/users/faq/faq.module
+++ b/sites/all/modules/contrib/users/faq/faq.module
@@ -18,6 +18,7 @@ function faq_help($path, $arg) {
'' . t("The 'Frequently Asked Questions' settings configuration screen will allow users with 'administer faq' permissions to specify different layouts of the questions and answers.") . '
' .
'' . t("All users with 'view faq page' permissions will be able to view the generated FAQ page.") . '
';
return $output;
+
case 'admin/modules#description':
return t('Allows the user to configure the layout of questions and answers on a FAQ page.');
}
@@ -183,7 +184,7 @@ function faq_menu() {
* Implements hook_node_info().
*
* Defines the FAQ node/content type.
- * @return
+ * @return array
* An array, containing the title, module name and the description.
*/
function faq_node_info() {
@@ -192,6 +193,7 @@ function faq_node_info() {
'name' => t('FAQ'),
'base' => 'faq',
'description' => t('A frequently asked question and its answer.'),
+ 'has_title' => TRUE,
'title_label' => t('Question'),
),
);
@@ -200,77 +202,41 @@ function faq_node_info() {
/**
* Implements hook_form().
*
- * @param &$node
+ * @param object $node
* The node being added or edited.
- * @param &$param
+ *
+ * @param array $form_state
* The hook can set this variable to an associative array of attributes to add
* to the enclosing ';
$depth--;
}
-
}
/**
@@ -739,6 +788,9 @@ function _display_faq_by_category($faq_display, $category_display, $term, $displ
function faq_theme() {
$path = drupal_get_path('module', 'faq') . '/includes';
return array(
+ 'field_detailed_question' => array(
+ 'render element' => 'element',
+ ),
'faq_draggable_question_order_table' => array(
'render element' => 'form',
),
@@ -752,13 +804,27 @@ function faq_theme() {
'path' => $path,
'file' => 'faq.questions_top.inc',
'template' => 'faq-category-questions-top',
- 'variables' => array('data' => NULL, 'display_header' => 0, 'category_display' => NULL, 'term' => NULL, 'class' => NULL, 'parent_term' => NULL),
+ 'variables' => array(
+ 'data' => NULL,
+ 'display_header' => 0,
+ 'category_display' => NULL,
+ 'term' => NULL,
+ 'class' => NULL,
+ 'parent_term' => NULL,
+ ),
),
'faq_category_questions_top_answers' => array(
'path' => $path,
'file' => 'faq.questions_top.inc',
'template' => 'faq-category-questions-top-answers',
- 'variables' => array('data' => NULL, 'display_header' => 0, 'category_display' => NULL, 'term' => NULL, 'class' => NULL, 'parent_term' => NULL),
+ 'variables' => array(
+ 'data' => NULL,
+ 'display_header' => 0,
+ 'category_display' => NULL,
+ 'term' => NULL,
+ 'class' => NULL,
+ 'parent_term' => NULL,
+ ),
),
'faq_hide_answer' => array(
'path' => $path,
@@ -770,7 +836,14 @@ function faq_theme() {
'path' => $path,
'file' => 'faq.hide_answer.inc',
'template' => 'faq-category-hide-answer',
- 'variables' => array('data' => NULL, 'display_header' => 0, 'category_display' => NULL, 'term' => NULL, 'class' => NULL, 'parent_term' => NULL),
+ 'variables' => array(
+ 'data' => NULL,
+ 'display_header' => 0,
+ 'category_display' => NULL,
+ 'term' => NULL,
+ 'class' => NULL,
+ 'parent_term' => NULL,
+ ),
),
'faq_questions_inline' => array(
'path' => $path,
@@ -782,7 +855,14 @@ function faq_theme() {
'path' => $path,
'file' => 'faq.questions_inline.inc',
'template' => 'faq-category-questions-inline',
- 'variables' => array('data' => NULL, 'display_header' => 0, 'category_display' => NULL, 'term' => NULL, 'class' => NULL, 'parent_term' => NULL),
+ 'variables' => array(
+ 'data' => NULL,
+ 'display_header' => 0,
+ 'category_display' => NULL,
+ 'term' => NULL,
+ 'class' => NULL,
+ 'parent_term' => NULL,
+ ),
),
'faq_new_page' => array(
'path' => $path,
@@ -794,10 +874,21 @@ function faq_theme() {
'path' => $path,
'file' => 'faq.new_page.inc',
'template' => 'faq-category-new-page',
- 'variables' => array('data' => NULL, 'display_header' => 0, 'category_display' => NULL, 'term' => NULL, 'class' => NULL, 'parent_term' => NULL),
+ 'variables' => array(
+ 'data' => NULL,
+ 'display_header' => 0,
+ 'category_display' => NULL,
+ 'term' => NULL,
+ 'class' => NULL,
+ 'parent_term' => NULL,
+ ),
),
'faq_page' => array(
- 'variables' => array('content' => '', 'answers' => '', 'description' => NULL),
+ 'variables' => array(
+ 'content' => '',
+ 'answers' => '',
+ 'description' => NULL,
+ ),
),
);
}
@@ -840,11 +931,17 @@ function faq_block_view($delta) {
$items[] = l(faq_tt("taxonomy:term:$tid:name", $name), 'faq-page/' . $tid);
}
$list_style = variable_get('faq_category_listing', 'ul');
- $block['content'] = theme('item_list', array('items' => $items, 'title' => NULL, 'type' => $list_style));
+ $block['content'] = theme('item_list',
+ array(
+ 'items' => $items,
+ 'title' => NULL,
+ 'type' => $list_style,
+ )
+ );
}
}
break;
- } // End switch($delta).
+ }
return $block;
}
@@ -852,11 +949,13 @@ function faq_block_view($delta) {
/**
* Return a HTML formatted list of terms indented according to the term depth.
*
- * @param $vid
+ * @param int $vid
* Vocabulary id.
- * @param $tid
+ *
+ * @param int $tid
* Term id.
- * @return
+ *
+ * @return string
* Return a HTML formatted list of terms indented according to the term depth.
*/
function _get_indented_faq_terms($vid, $tid) {
@@ -945,7 +1044,7 @@ function _get_indented_faq_terms($vid, $tid) {
/**
* Get a list of terms associated with the FAQ nodes.
*
- * @return
+ * @return string
* Return the HTML-formatted content.
*/
function faq_get_terms() {
@@ -957,13 +1056,14 @@ function faq_get_terms() {
$vocab_items = _get_indented_faq_terms($vid, 0);
$items = array_merge($items, $vocab_items);
}
+
return theme('item_list', array('items' => $items));
}
/**
* Format the output for the faq_site_map() function.
*
- * @return
+ * @return array
* Return a list of FAQ categories if categorization is enabled, otherwise
* return a list of faq nodes.
*/
@@ -994,8 +1094,12 @@ function faq_get_faq_list() {
// Works, but involves variable concatenation - safe though, since
// $default_weight is an integer.
$query->addExpression("COALESCE(w.weight, $default_weight)", 'effective_weight');
- // Doesn't work in Postgres.
+
+ // @codingStandardsIgnoreStart
+ // @todo Doesn't work in Postgres.
//$query->addExpression('COALESCE(w.weight, CAST(:default_weight as SIGNED))', 'effective_weight', array(':default_weight' => $default_weight));
+ // @codingStandardsIgnoreEnd
+
$query->orderBy('effective_weight', 'ASC')
->orderBy('n.sticky', 'DESC');
if ($default_sorting == 'ASC') {
@@ -1023,6 +1127,9 @@ function faq_get_faq_list() {
}
if (!function_exists('array_diff_key')) {
+ /**
+ * Override array_diff_key function.
+ */
function array_diff_key() {
$arrs = func_get_args();
$result = array_shift($arrs);
@@ -1040,13 +1147,16 @@ if (!function_exists('array_diff_key')) {
/**
* Helper function to setup the faq question.
*
- * @param &$data
+ * @param array &$data
* Array reference to store display data in.
- * @param $node
+ *
+ * @param object $node
* The node object.
- * @param $path
+ *
+ * @param string $path
* The path/url which the question should link to if links are disabled.
- * @param $anchor
+ *
+ * @param string $anchor
* Link anchor to use in question links.
*/
function faq_view_question(&$data, $node, $path = NULL, $anchor = NULL) {
@@ -1083,9 +1193,24 @@ function faq_view_question(&$data, $node, $path = NULL, $anchor = NULL) {
}
$question = '' . $question . '';
- if (variable_get('faq_display', 'questions_top') != 'hide_answer' && !empty($node->detailed_question) && variable_get('faq_question_length', 'short') == 'both') {
- $node->detailed_question = check_markup($node->detailed_question, 'filtered_html', '', FALSE);
- $question .= '' . $node->detailed_question . '';
+ // Get the language of the body field.
+ $language = 'und';
+ foreach ($node->body as $lang => $values) {
+ if ($values[0]['value']) {
+ $language = $lang;
+ }
+ }
+
+ // Get the detailed question.
+ $detailed_question = '';
+ if ($dq = field_get_items('node', $node, 'field_detailed_question')) {
+ $detailed_question = reset($dq);
+ }
+
+ if (variable_get('faq_display', 'questions_top') != 'hide_answer'
+ && !empty($detailed_question['value'])
+ && variable_get('faq_question_length', 'short') == 'both') {
+ $question .= '' . $detailed_question['safe_value'] . '';
}
$data['question'] = $question;
}
@@ -1093,19 +1218,22 @@ function faq_view_question(&$data, $node, $path = NULL, $anchor = NULL) {
/**
* Helper function to setup the faq answer.
*
- * @param &$data
+ * @param array &$data
* Array reference to store display data in.
- * @param $node
+ *
+ * @param object $node
* The node object.
- * @param $back_to_top
+ *
+ * @param array $back_to_top
* An array containing the "back to top" link.
- * @param $teaser
+ *
+ * @param bool $teaser
* Whether or not to use teasers.
- * @param $links
+ *
+ * @param array $links
* Whether or not to show node links.
*/
function faq_view_answer(&$data, $node, $back_to_top, $teaser, $links) {
-
$view_mode = $teaser ? 'teaser' : 'full';
$langcode = $GLOBALS['language_content']->language;
@@ -1149,14 +1277,12 @@ function faq_view_answer(&$data, $node, $back_to_top, $teaser, $links) {
$node_links = ($links ? $build['links']['node']['#links'] : (!empty($back_to_top) ? array($build['links']['node']['#links']['faq_back_to_top']) : NULL));
unset($build['links']);
- unset($build['#theme']); // We don't want node title displayed.
+
+ // We don't want node title displayed.
+ unset($build['#theme']);
$content = drupal_render($build);
- // Unset unused $node text so that a bad theme can not open a security hole.
- // $node->body = NULL;
- // $node->teaser = NULL;
-
$data['body'] = $content;
$data['links'] = !empty($node_links) ? theme('links', array('links' => $node_links, 'attributes' => array('class' => 'links inline'))) : '';
}
@@ -1164,11 +1290,12 @@ function faq_view_answer(&$data, $node, $back_to_top, $teaser, $links) {
/**
* Helper function to setup the "back to top" link.
*
- * @param $path
+ * @param string $path
* The path/url where the "back to top" link should bring the user too. This
* could be the 'faq-page' page or one of the categorized faq pages, e.g 'faq-page/123'
* where 123 is the tid.
- * @return
+ *
+ * @return array
* An array containing the "back to top" link.
*/
function faq_init_back_to_top($path) {
@@ -1190,22 +1317,31 @@ function faq_init_back_to_top($path) {
/**
* Helper function for retrieving the sub-categories faqs.
*
- * @param $term
+ * @param object $term
* The category / term to display FAQs for.
- * @param $theme_function
+ *
+ * @param string $theme_function
* Theme function to use to format the Q/A layout for sub-categories.
- * @param $default_weight
+ *
+ * @param int $default_weight
* Is 0 for $default_sorting = DESC; is 1000000 for $default_sorting = ASC.
- * @param $default_sorting
+ *
+ * @param string $default_sorting
* If 'DESC', nodes are sorted by creation date descending; if 'ASC', nodes
* are sorted by creation date ascending.
- * @param $category_display
+ *
+ * @param string $category_display
* The layout of categories which should be used.
- * @param $class
+ *
+ * @param string $class
* CSS class which the HTML div will be using. A special class name is
* required in order to hide and questions / answers.
- * @param $parent_term
+ *
+ * @param string $parent_term
* The original, top-level, term we're displaying FAQs for.
+ *
+ * @return string
+ * Returns markup.
*/
function faq_get_child_categories_faqs($term, $theme_function, $default_weight, $default_sorting, $category_display, $class, $parent_term = NULL) {
$output = array();
@@ -1237,8 +1373,12 @@ function faq_get_child_categories_faqs($term, $theme_function, $default_weight,
// Works, but involves variable concatenation - safe though, since
// $default_weight is an integer.
$query->addExpression("COALESCE(w.weight, $default_weight)", 'effective_weight');
- // Doesn't work in Postgres.
+
+ // @codingStandardsIgnoreStart
+ // @todo Doesn't work in Postgres.
//$query->addExpression('COALESCE(w.weight, CAST(:default_weight as SIGNED))', 'effective_weight', array(':default_weight' => $default_weight));
+ // @codingStandardsIgnoreEnd
+
$query->orderBy('effective_weight', 'ASC')
->orderBy('n.sticky', 'DESC');
if ($default_sorting == 'ASC') {
@@ -1257,7 +1397,16 @@ function faq_get_child_categories_faqs($term, $theme_function, $default_weight,
$nids = $query->execute()->fetchCol();
$data = node_load_multiple($nids);
- $output[] = theme($theme_function, array('data' => $data, 'display_header' => 1, 'category_display' => $category_display, 'term' => $child_term, 'class' => $class, 'parent_term' => $parent_term));
+ $output[] = theme($theme_function,
+ array(
+ 'data' => $data,
+ 'display_header' => 1,
+ 'category_display' => $category_display,
+ 'term' => $child_term,
+ 'class' => $class,
+ 'parent_term' => $parent_term,
+ )
+ );
}
}
@@ -1267,13 +1416,13 @@ function faq_get_child_categories_faqs($term, $theme_function, $default_weight,
/**
* Helper function to setup the list of sub-categories for the header.
*
- * @param $term
+ * @param object $term
* The term to setup the list of child terms for.
- * @return
+ *
+ * @return array
* An array of sub-categories.
*/
function faq_view_child_category_headers($term) {
-
$child_categories = array();
$list = taxonomy_get_children($term->tid);
@@ -1311,8 +1460,8 @@ function faq_pathauto($op) {
$settings['patterndefault'] = t('faq-page/[term:tid]');
$settings['batch_update_callback'] = 'faq_pathauto_bulkupdate';
$settings['token_type'] = 'term';
-
return (object) $settings;
+
default:
break;
}
@@ -1435,8 +1584,11 @@ function faq_taxonomy_term_delete($term) {
/**
* Function to set up the FAQ breadcrumbs for a given taxonomy term.
*
- * @param $term
+ * @param object $term
* The taxonomy term object.
+ *
+ * @return array|NULL
+ * Breadcrumbs.
*/
function faq_set_breadcrumb($term = NULL) {
$breadcrumb = array();
@@ -1499,10 +1651,16 @@ function faq_filter_info() {
return $filters;
}
+/**
+ * Filter settings.
+ */
function _faq_filter_settings($form, &$form_state, $filter, $format, $defaults, $filters) {
return array();
}
+/**
+ * Filter string.
+ */
function _faq_filter_process($text) {
$text = preg_replace_callback('/\[faq:?([^\]]*)\]/', '_faq_faq_page_filter_replacer', $text);
// Remove comments, as they're not supported by all input formats.
@@ -1524,6 +1682,13 @@ function _faq_faq_page_filter_replacer($matches) {
$tid = 0;
$faq_display = '';
$category_display = '';
+ $default_display = array(
+ 'questions_top',
+ 'hide_answer',
+ 'questions_inline',
+ 'new_page',
+ );
+ $default_category_display = array('hide_qa', 'new_page', 'categories_inline');
if (drupal_strlen($matches[1])) {
list($tid, $faq_display, $category_display) = explode(',', $matches[1] . ',,');
$tid = (int) trim($tid);
@@ -1531,10 +1696,10 @@ function _faq_faq_page_filter_replacer($matches) {
$category_display = trim($category_display);
// These two checks ensure that a typo in the faq_display or
// category_display string still results in the FAQ showing.
- if ($faq_display && !in_array($faq_display, array('questions_top', 'hide_answer', 'questions_inline', 'new_page'))) {
+ if ($faq_display && !in_array($faq_display, $default_display)) {
$faq_display = '';
}
- if ($category_display && !in_array($category_display, array('hide_qa', 'new_page', 'categories_inline'))) {
+ if ($category_display && !in_array($category_display, $default_category_display)) {
$category_display = '';
}
}
@@ -1607,6 +1772,34 @@ function theme_faq_page($variables) {
return $output;
}
+/**
+ * Theme function for the detailed questionfield.
+ *
+ * @param array $variables
+ * Variables array.
+ *
+ * @return string
+ * Markup.
+ */
+function theme_field_detailed_question($variables) {
+ // Render the label, if it's not hidden.
+ $output = '';
+
+ if (isset($variables['label']) && !$variables['label_hidden'] && $variables['label']) {
+ $output .= '' . $variables['label'] . ': ';
+ }
+
+ // Render the items.
+ $output .= '' . $variables['safe_value'] . '';
+
+ // Render the top-level DIV.
+ if (isset($variables['classes']) && isset($variables['attributes'])) {
+ $output = '' . $output . '';
+ }
+
+ return $output;
+}
+
/**
* Theme function for question ordering drag and drop table.
*/
@@ -1628,8 +1821,13 @@ function theme_faq_draggable_question_order_table($variables) {
'class' => array('draggable'),
);
}
- $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'question-sort')));
+ $output = theme('table',
+ array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => 'question-sort'),
+ )
+ );
$output .= drupal_render_children($form);
return $output;
}
-
diff --git a/sites/all/modules/contrib/users/faq/faq.test b/sites/all/modules/contrib/users/faq/faq.test
index 809d8027..ff58f669 100644
--- a/sites/all/modules/contrib/users/faq/faq.test
+++ b/sites/all/modules/contrib/users/faq/faq.test
@@ -207,12 +207,21 @@ class CreateFaqTestCase extends FaqTestCase {
/**
* Test creating a FAQ node
+ *
+ * This test creates a faq with detailed question shown and labeled questions and answers
*/
function testFaqCreate() {
// Create and log in user with create FAQ permissions
- $this->admin_user = $this->drupalCreateUser(array('create faq content', 'view faq page'));
$this->drupalLogin($this->admin_user);
+ // Show the detailed question
+ $this->drupalGet('admin/config/content/faq/questions');
+ // Set faq to allow long question text and labeled questions and answers
+ $this->drupalPost('admin/config/content/faq/questions', array('faq_question_long_form' => '1', 'faq_qa_mark' => '1'), t('Save configuration'));
+ $this->drupalPost('admin/config/content/faq/questions', array('faq_question_length' => 'both'), t('Save configuration'));
+
+ $this->afaq_user = $this->drupalCreateUser(array('create faq content', 'view faq page'));
+ $this->drupalLogin($this->afaq_user);
// Verify that the faq page is visible and available but empty
$this->drupalGet('faq-page');
@@ -223,18 +232,23 @@ class CreateFaqTestCase extends FaqTestCase {
$this->faq1 = array();
$this->faq1['title'] = 'faq1_'. $this->randomName(8);
$this->faq1[$this->instance['field_name'] . '[' . $langcode .']'] = $this->term->name;
- $this->faq1['detailed_question'] = $this->randomName(16);
+ $this->faq1["field_detailed_question[$langcode][0][value]"] = $this->randomName(16);
$this->faq1["body[$langcode][0][value]"] = $this->randomName(16);
$this->drupalPost('node/add/faq', $this->faq1, t('Save'));
+
// Check that new FAQ node has actually been created
$this->assertText(t('FAQ @title has been created.', array('@title' => $this->faq1['title'])));
+ // Check that faq is found on the correct taxonomy term page too
+ $this->drupalGet('taxonomy/term/' . $this->term->tid);
+ $this->assertText(t('@title', array('@title' => $this->faq1['title'])));
+
// Fill in the Create FAQ node 2 form and post it
$this->faq2 = array();
$this->faq2['title'] = 'faq2_'. $this->randomName(8);
$this->faq2[$this->instance['field_name'] . '[' . $langcode .']'] = $this->randomName(8); // Add new term
- $this->faq2['detailed_question'] = $this->randomName(16);
+ $this->faq2["field_detailed_question[$langcode][0][value]"] = $this->randomName(16);
$this->faq2["body[$langcode][0][value]"] = $this->randomName(16);
$this->drupalPost('node/add/faq', $this->faq2, t('Save'));
@@ -261,7 +275,8 @@ class CreateFaqTestCase extends FaqTestCase {
// Navigate to FAQ node created on FAQ page
$this->clickLink(t($this->faq1['title']));
- $this->assertText(t($this->faq1["body[$langcode][0][value]"]));
+ $this->assertText(t($this->faq1["field_detailed_question[$langcode][0][value]"]), t('Detailed question visible')); // Dependant on the question setting
+ $this->assertText(t($this->faq1["body[$langcode][0][value]"]), t('Answer visible'));
// Enable categorisation of FAQ nodes
// Create and log in user with create and administer FAQ permissions
@@ -317,7 +332,7 @@ class CRAUDFaqTestCase extends FaqTestCase {
$edit = array();
$edit['title'] = $this->randomName(8);
$edit[$this->instance['field_name'] . '['. LANGUAGE_NONE .']'] = $this->randomName(8);
- $edit['detailed_question'] = $this->randomName(64);
+ $edit['field_detailed_question['. LANGUAGE_NONE .'][0][value]'] = $this->randomName(64);
$edit['body['. LANGUAGE_NONE .'][0][value]'] = $this->randomString(264);
$this->drupalPost('node/add/faq', $edit, t('Save'));
$this->assertText(t('FAQ @title has been created.', array('@title' => $edit['title'])));
diff --git a/sites/all/modules/contrib/users/faq/faq.variable.inc b/sites/all/modules/contrib/users/faq/faq.variable.inc
index 4d48d7c4..357b2e89 100644
--- a/sites/all/modules/contrib/users/faq/faq.variable.inc
+++ b/sites/all/modules/contrib/users/faq/faq.variable.inc
@@ -1,5 +1,10 @@
0) {
$hdr = 'h4';
}
else {
$hdr = 'h3';
}
+// @codingStandardsIgnoreEnd
?>
-
-
diff --git a/sites/all/modules/contrib/users/faq/includes/faq-category-new-page.tpl.php b/sites/all/modules/contrib/users/faq/includes/faq-category-new-page.tpl.php
index 18ca3aaa..bec7275f 100644
--- a/sites/all/modules/contrib/users/faq/includes/faq-category-new-page.tpl.php
+++ b/sites/all/modules/contrib/users/faq/includes/faq-category-new-page.tpl.php
@@ -46,12 +46,15 @@
* The sub-categories faqs, recursively themed (by this template).
*/
+// @todo should be moved to better place.
+// @codingStandardsIgnoreStart
if ($category_depth > 0) {
$hdr = 'h4';
}
else {
$hdr = 'h3';
}
+// @codingStandardsIgnoreEnd
?>
-
-
-
diff --git a/sites/all/modules/contrib/users/faq/includes/faq-category-questions-inline.tpl.php b/sites/all/modules/contrib/users/faq/includes/faq-category-questions-inline.tpl.php
index 921c935c..926ffd8e 100644
--- a/sites/all/modules/contrib/users/faq/includes/faq-category-questions-inline.tpl.php
+++ b/sites/all/modules/contrib/users/faq/includes/faq-category-questions-inline.tpl.php
@@ -53,13 +53,15 @@
* The sub-categories faqs, recursively themed (by this template).
*/
+// @todo should be moved to better place.
+// @codingStandardsIgnoreStart
if ($category_depth > 0) {
$hdr = 'h4';
}
else {
$hdr = 'h3';
}
-
+// @codingStandardsIgnoreEnd
?>
@@ -143,7 +145,5 @@ else {
-
-
diff --git a/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top-answers.tpl.php b/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top-answers.tpl.php
index d03539ec..c3fe6261 100644
--- a/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top-answers.tpl.php
+++ b/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top-answers.tpl.php
@@ -53,17 +53,19 @@
* The sub-categories faqs, recursively themed (by this template).
*/
-
+// @todo should be moved to better place.
+// @codingStandardsIgnoreStart
if ($category_depth > 0) {
$hdr = 'h4';
}
else {
$hdr = 'h3';
}
-
$depth = 0;
+// @codingStandardsIgnoreEnd
+?>
-?>
+
@@ -135,5 +137,4 @@ $depth = 0;
-
diff --git a/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top.tpl.php b/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top.tpl.php
index 67ae3123..67be2ab8 100644
--- a/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top.tpl.php
+++ b/sites/all/modules/contrib/users/faq/includes/faq-category-questions-top.tpl.php
@@ -60,13 +60,15 @@
* The sub-categories faqs, recursively themed (by this template).
*/
+// @todo should be moved to better place.
+// @codingStandardsIgnoreStart
if ($category_depth > 0) {
$hdr = 'h4';
}
else {
$hdr = 'h3';
}
-
+// @codingStandardsIgnoreEnd
?>